# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 2001 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

import operator, os
from types import StringType, TupleType, IntType
from string import strip, split, atof, atoi

import X

from Sketch.const import CHANGED, COLOR1, COLOR2, CHANGED, VIEW, \
     DROP_COLOR, CurDragColor
from Sketch.warn import warn, INTERNAL, USER, pdebug, warn_tb
from Sketch import Publisher, config, SketchError, _
from Sketch import CreateRGBColor, StandardColors, GraphicsDevice, Identity

from tkext import PyWidget


class NameInUse(SketchError):
    pass

class RGBAlreadyStored(SketchError):
    pass

class RGBPalette(Publisher):

    ignore_issue = 1

    def __init__(self):
	self.entries = []
	self.name_to_entry = {}
	self.rgb_to_entry = {}

    def Subscribe(self, channel, func, *args):
	apply(Publisher.Subscribe, (self, channel, func) + args)
	self.ignore_issue = 0

    def update_dicts(self):
	self.name_to_entry = {}
	self.rgb_to_entry = {}
	for entry in self.entries:
	    rgb, name = entry
	    self.name_to_entry[name] = entry
	    self.rgb_to_entry[rgb] = entry

    def AddEntry(self, rgb, name = None, rename = 0):
	if name:
	    if self.name_to_entry.has_key(name):
		if self.name_to_entry[name] != (rgb, name):
		    raise NameInUse
	if self.rgb_to_entry.has_key(rgb):
	    if self.rgb_to_entry[rgb] != (rgb, name) and not rename:
		raise RGBAlreadyStored
	if not name:
	    i = 0
	    base = 'Color '
	    name = base + `i`
	    known = self.name_to_entry.has_key
	    while known(name):
		i = i + 1
		name = base + `i`
	entry = (rgb, name)
	self.entries.append(entry)
	self.name_to_entry[name] = entry
	self.rgb_to_entry[rgb] = entry
	self.issue(CHANGED)

    def __getitem__(self, idx):
	if type(idx) == StringType:
	    return self.name_to_entry[idx]
	if type(idx) == TupleType:
	    return self.rgb_to_entry[idx]
	if type(idx) == IntType:
	    return self.entries[idx]


    def GetRGB(self, idx):
	return self[idx][0]

    def Colors(self):
	return map(operator.getitem, self.entries, [0] * len(self.entries))

    def WriteFile(self, file):
	for entry in self.entries:
	    (r, g, b), name = entry
	    file.write('%g %g %g\t%s\n' % (r, g, b, name))

    def __len__(self):
	return len(self.entries)



#
#	Get the standard palette. User settable.
#

def read_standard_palette(filename):
    filename = os.path.join(config.std_res_dir, filename)
    return read_palette_file(filename)

#minimalistic fallback:
_mini_pal = [(0, 0, 0, 'Black'),
	     (1, 1, 1, 'White')]

def GetStandardPalette():
    palette = read_standard_palette(config.preferences.palette)
    if not palette:
	warn(USER, _("Could not load palette %s; trying mini.spl..."),
	     config.preferences.palette)
	palette = read_standard_palette('mini.spl')
	if not palette:
	    warn(USER,
		_("Could not load palette mini.spl; reverting to black&white"))
	    palette = RGBPalette()
	    for r, g, b, name in _mini_pal:
		palette.AddEntry((r, g, b), name)
    return palette


def LoadPalette(filename):
    return read_palette_file(filename)

file_types = ((_("Sketch Palette"), '.spl'),
	      (_("All Files"),	 '*'))


magic_rgb_palette = '##Sketch RGBPalette 0'
magic_gimp_palette = 'GIMP Palette'

def read_palette_file(filename):
    """Read the palette file filename"""
    file = open(filename)
    line = file.readline()
    line = strip(line)
    palette = None
    try:
        if line == magic_rgb_palette:
            palette = ReadRGBPaletteFile(filename)
        elif line == magic_gimp_palette:
            palette = Read_X_RGB_TXT(filename)
    except:
	warn_tb(USER)
    return palette


def ReadRGBPaletteFile(filename):
    file = open(filename)

    line = file.readline()
    if line != magic_rgb_palette + '\n':
	file.close()
	raise ValueError, 'Invalid file type'

    palette = RGBPalette()

    linenr = 1
    for line in file.readlines():
	line = strip(line)
	linenr = linenr + 1
	if not line or line[0] == '#':
	    continue

	line = split(line, None, 3)
               
	if len(line) != 4:
	    warn(INTERNAL, '%s:%d: wrong number of fields', filename, linenr)
	    continue
	try:
	    rgb = tuple(map(atof, line[:3]))
	except:
	    warn(INTERNAL, '%s:%d: cannot parse rgb values', filename, linenr)
	    continue

	for value in rgb:
	    if value < 0 or value > 1.0:
		warn(INTERNAL, '%s:%d: value out of range', filename, linenr)
		continue

	name = strip(line[-1])

	try:
	    palette.AddEntry(rgb, name)
	except NameInUse:
	    warn(INTERNAL, '%s:%d: color name already used', filename, linenr)
	    continue
	except RGBAlreadyStored:
	    warn(INTERNAL, '%s:%d: color already stored', filename, linenr)
	    continue

    file.close()

    return palette



def Read_X_RGB_TXT(filename):
    file = open(filename)

    palette = RGBPalette()

    linenr = 0
    color_num = 0
    for line in file.readlines():
	line = strip(line)
	linenr = linenr + 1
	if not line or line[0] in ('#', '!'):
	    # an empty line or an X-style comment (!) or a GIMP comment (#)
	    # GIMP's palette files have practically the same format as rgb.txt
	    continue

	line = split(line, None, 3)
        if len(line) == 3:
            # the name is missing
            while 1:
                name = 'color ' + str(color_num)
                try:
                    palette[name]
                    used = 1
                except KeyError:
                    used = 0
                if not used:
                    line.append(name)
                    break
                color_num = color_num + 1
	if len(line) != 4:
	    warn(INTERNAL, '%s:%d: wrong number of fields', filename, linenr)
	    continue
	try:
	    values = map(atoi, line[:3])
	except:
	    warn(INTERNAL, '%s:%d: cannot parse rgb values', filename, linenr)
	    continue

	rgb = []
	for value in values:
	    value = round(value / 255.0, 3)
	    if value < 0:
		value = 0.0
	    elif value > 1.0:
		value = 1.0
	    rgb.append(value)
	rgb = tuple(rgb)

	name = strip(line[-1])

	try:
	    palette.AddEntry(rgb, name)
	except NameInUse:
	    warn(INTERNAL, '%s:%d: color name already used', filename, linenr)
	    continue
	except RGBAlreadyStored:
	    warn(INTERNAL, '%s:%d: color already stored', filename, linenr)
	    continue

    file.close()

    return palette



class PaletteWidget(PyWidget, Publisher):

    def __init__(self, master=None, palette = None, cell_size = 16, **kw):
	if not kw.has_key('height'):
	    kw['height'] = cell_size
	apply(PyWidget.__init__, (self, master), kw)

	self.cell_size = cell_size
	self.num_cells = 0
	self.gc_initialized = 0
	self.gc = GraphicsDevice()
	self.gc.SetViewportTransform(1.0, Identity, Identity)
	self.start_idx = 0
	self.palette = None
	if palette is None:
	    palette = RGBPalette()
	self.SetPalette(palette)
	self.dragging = 0
	self.bind('<ButtonPress-1>', self.press_1)
	self.bind('<Motion>', self.move_1)
	self.bind('<ButtonRelease-1>', self.release_1)
	self.bind('<ButtonRelease-2>', self.apply_color_2)

    def DestroyMethod(self):
	self.palette.Unsubscribe(CHANGED, self.palette_changed)
	Publisher.Destroy(self)

    def compute_num_cells(self):
	self.num_cells = self.tkwin.width / self.cell_size

    def MapMethod(self):
	self.compute_num_cells()
	self.issue(VIEW)
	if not self.gc_initialized:
	    self.init_gc()
	    self.gc_initialized = 1

    def init_gc(self):
	self.gc.init_gc(self.tkwin)

    def get_color(self, x, y):
	if 0 <= x < self.tkwin.width and 0 <= y < self.tkwin.height:
	    i = self.start_idx + x / self.cell_size
	    if i < len(self.palette):
		return apply(CreateRGBColor, self.palette.GetRGB(i))

    def release_1(self, event):
	try:
	    if self.dragging:
		self.drop_color(event)
	    else:
		self.apply_color_1(event)
	finally:
	    self.dragging = 0

    def drop_color(self, event):
	self['cursor'] = self.drag_old_cursor
	w = self.winfo_containing(event.x_root, event.y_root)
	while w and w != self:
	    if __debug__:
		pdebug('DND', 'trying to drop on', w)
	    try:
		accepts = w.accept_drop
	    except AttributeError:
		accepts = ()
	    if DROP_COLOR in accepts:
		x = event.x_root - w.winfo_rootx()
		y = event.y_root - w.winfo_rooty()
		w.DropAt(x, y, DROP_COLOR, self.drag_start)
		break
	    if w != w.winfo_toplevel():
		parent = self.tk.call('winfo', 'parent', w._w)
		w = self.nametowidget(parent)
	    else:
		break


    def apply_color_1(self, event):
	c = self.get_color(event.x, event.y)
	if c:
	    self.issue(COLOR1, c)

    def apply_color_2(self, event):
	c = self.get_color(event.x, event.y)
	if c:
	    self.issue(COLOR2, c)

    drag_start = (0, 0, 0)
    def press_1(self, event):
	self.drag_start = self.get_color(event.x, event.y)

    def move_1(self, event):
	if event.state & X.Button1Mask:
	    if not self.dragging:
		self.dragging = 1
		self.drag_old_cursor = self['cursor']
		self['cursor'] = CurDragColor
	    w = self.winfo_containing(event.x_root, event.y_root)

    def Palette(self):
	return self.palette

    def SetPalette(self, palette):
	if self.palette is not None:
	    self.palette.Unsubscribe(CHANGED, self.palette_changed)
	self.palette = palette
	self.palette.Subscribe(CHANGED, self.palette_changed)
	self.palette_changed()

    def palette_changed(self):
	self.compute_num_cells()
	self.normalize_start()
	self.issue(VIEW)
	self.UpdateWhenIdle()

    def RedrawMethod(self, region = None):
	win = self.tkwin
	width = win.width
	height = win.height
	self.gc.StartDblBuffer()
	self.gc.SetFillColor(StandardColors.white)
	self.gc.FillRectangle(0, 0, width, height)

	x = 0
	FillRectangle = self.gc.FillRectangle
	SetFillColor = self.gc.SetFillColor
	create_color = CreateRGBColor
	rgbs = self.palette.Colors()
	rgbs = rgbs[self.start_idx:self.start_idx + self.num_cells]
	for rgb in rgbs:
	    SetFillColor(apply(create_color, rgb))
	    FillRectangle(x, 0, x + height, height)
	    x = x + height
	self.gc.EndDblBuffer()

    def ResizedMethod(self, width, height):
	self.compute_num_cells()
	self.gc.WindowResized(width, height)
	self.normalize_start()
	self.UpdateWhenIdle()

    def normalize_start(self):
	length = len(self.palette)
	if self.start_idx < 0:
	    self.start_idx = 0
	if length < self.num_cells:
	    self.start_idx = 0
	elif length - self.start_idx < self.num_cells:
	    self.start_idx = length - self.num_cells

    def CanScrollLeft(self):
	return self.start_idx > 0

    def CanScrollRight(self):
	return len(self.palette) - self.start_idx > self.num_cells

    def ScrollXPages(self, count):
	length = self.tkwin.width / self.cell_size
	start = self.start_idx
	self.start_idx = self.start_idx + count * length
	self.normalize_start()
	if start != self.start_idx:
	    self.UpdateWhenIdle()
	    self.issue(VIEW)

    def ScrollXUnits(self, count):
	start = self.start_idx
	self.start_idx = self.start_idx + count
	self.normalize_start()
	if start != self.start_idx:
	    self.UpdateWhenIdle()
	    self.issue(VIEW)

