# This file is part of MyPaint.
# Copyright (C) 2007 by Martin Renold <martinxyz@gmx.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY. See the COPYING file for more details.

"interface to MyBrush; hiding some C implementation details"
# FIXME: bad file name, saying nothing about what's in here
import mydrawwidget
import brushsettings
import gtk, string, os, colorsys
from helpers import clamp

thumb_w = 64 #128
thumb_h = 64 #128

current_brushfile_version = 2

def pixbuf_scale_nostretch_centered(src, dst):
    scale_x = float(dst.get_width()) / src.get_width()
    scale_y = float(dst.get_height()) / src.get_height()
    offset_x = 0
    offset_y = 0
    if scale_x > scale_y: 
        scale = scale_y
        offset_x = (dst.get_width() - src.get_width() * scale) / 2
    else:
        scale = scale_x
        offset_y = (dst.get_height() - src.get_height() * scale) / 2

    src.scale(dst, 0, 0, dst.get_width(), dst.get_height(),
              offset_x, offset_y, scale, scale,
              gtk.gdk.INTERP_BILINEAR)

# points = [(x1, y1), (x2, y2), ...] (at least two points, or None)
class Setting:
    "a specific setting for a specific brush"
    def __init__(self, setting, parent_brush, observers):
        self.setting = setting
        self.brush = parent_brush
        self.observers = observers
        self.base_value = None
        self.set_base_value(setting.default)
        self.points = [[] for i in xrange(len(brushsettings.inputs))]
        if setting.cname == 'opaque_multiply':
            # make opaque depend on pressure by default
            for i in brushsettings.inputs:
                if i.name == 'pressure': break
            self.set_points(i, [(0.0, 0.0), (1.0, 1.0)])
    def set_base_value(self, value):
        if self.base_value == value: return
        self.base_value = value
        self.brush.set_base_value(self.setting.index, value)
        for f in self.observers: f()
    def has_only_base_value(self):
        for i in brushsettings.inputs:
            if self.has_input(i):
                return False
        return True
    def has_input(self, input):
        return self.points[input.index]
    def has_input_nonlinear(self, input):
        points = self.points[input.index]
        if not points: return False
        if len(points) > 2: return True
        # also if it is linear but the x-axis was changed (hm, bad function name)
        if abs(points[0][0] - input.soft_min) > 0.001: return True
        if abs(points[1][0] - input.soft_max) > 0.001: return True
        return False

    def set_points(self, input, points):
        assert len(points) != 1
        if self.points[input.index] == points: return
        #if len(points) > 2:
        #    print 'set_points[%s](%s, %s)' % (self.setting.cname, input.name, points)

        self.brush.set_mapping_n(self.setting.index, input.index, len(points))
        for i, (x, y) in enumerate(points):
            self.brush.set_mapping_point(self.setting.index, input.index, i, x, y)

        self.points[input.index] = points[:] # copy
        for f in self.observers: f()

    def copy_from(self, other):
        error = self.load_from_string(other.save_to_string(), version=current_brushfile_version)
        assert not error, error
    def save_to_string(self):
        s = str(self.base_value)
        for i in brushsettings.inputs:
            points = self.points[i.index]
            if points:
                s += ' | ' + i.name + ' ' + ', '.join(['(%f %f)' % xy for xy in points])
        return s
    def load_from_string(self, s, version):
        error = None
        parts = s.split('|')
        self.set_base_value(float(parts[0]))
        for i in brushsettings.inputs:
            self.set_points(i, [])
        for part in parts[1:]:
            command, args = part.strip().split(' ', 1)
            if version <= 1 and command == 'speed': command = 'speed1'
            i = brushsettings.inputs_dict.get(command)
            if i:
                if version <= 1:
                    points_old = [float(f) for f in args.split()]
                    points = [(0, 0)]
                    while points_old:
                        x = points_old.pop(0)
                        y = points_old.pop(0)
                        if x == 0: break
                        assert x > points[-1][0]
                        points.append((x, y))
                else:
                    points = []
                    for s in args.split(', '):
                        s = s.strip()
                        if not (s.startswith('(') and s.endswith(')') and ' ' in s):
                            return '(x y) expected, got "%s"' % s
                        s = s[1:-1]
                        try:
                            x, y = [float(ss) for ss in s.split(' ')]
                        except:
                            print s
                            raise
                        points.append((x, y))
                assert len(points) >= 2
                self.set_points(i, points)
            else:
                error = 'unknown input "%s"' % command
        return error
    def transform_y(self, func):
        # useful for migration from a earlier version
        self.set_base_value(func(self.base_value))
        for i in brushsettings.inputs:
            if not self.points[i.index]: continue
            points = self.points[i.index]
            points = [(x, func(y)) for x, y in points]
            self.set_points(i, points)

class Brush_Lowlevel(mydrawwidget.MyBrush):
    def __init__(self):
        mydrawwidget.MyBrush.__init__(self)
        self.observers = []
        self.hidden_observers = []
        self.settings = []
        for s in brushsettings.settings:
            self.settings.append(Setting(s, self, self.observers))

        self.saved_string = None
        self.observers.append(self.invalidate_saved_string)

    def invalidate_saved_string(self):
        self.saved_string = None

    def begin_atomic(self):
        self.hidden_observers.append(self.observers[:])
        del self.observers[:]

    def end_atomic(self):
        self.observers[:] = self.hidden_observers.pop()
        for f in self.observers: f()

    def setting_by_cname(self, cname):
        s = brushsettings.settings_dict[cname]
        return self.settings[s.index]

    def save_to_string(self):
        if self.saved_string: return self.saved_string
        res  = '# mypaint brush file\n'
        res += '# you can edit this file and then select the brush in mypaint (again) to reload\n'
        res += 'version %d\n' % current_brushfile_version
        for s in brushsettings.settings:
            res += s.cname + ' ' + self.settings[s.index].save_to_string() + '\n'
        self.saved_string = res
        return res

    def load_from_string(self, s):
        self.begin_atomic()
        num_found = 0
        errors = []
        version = 1 # for files without a 'version' field
        for line in s.split('\n'):
            line = line.strip()
            if line.startswith('#'): continue
            if not line: continue
            try:
                command, rest = line.split(' ', 1)
                error = None

                if command in brushsettings.settings_dict:
                    setting = self.setting_by_cname(command)
                    error = setting.load_from_string(rest, version)
                elif command in brushsettings.settings_migrate:
                    command_new, transform_func = brushsettings.settings_migrate[command]
                    setting = self.setting_by_cname(command_new)
                    error = setting.load_from_string(rest, version)
                    if transform_func:
                        setting.transform_y(transform_func)
                elif command == 'version':
                    version = int(rest)
                    if version > current_brushfile_version:
                        error = 'this brush was saved with a more recent version of mypaint'
                elif version <= 1 and command == 'color':
                    self.set_color_rgb([int(s)/255.0 for s in rest.split()])
                elif version <= 1 and command == 'change_radius':
                    if rest != '0.0': error = 'change_radius is not supported any more'
                elif version <= 2 and command == 'adapt_color_from_image':
                    if rest != '0.0': error = 'adapt_color_from_image is obsolete, ignored; use smudge and smudge_length instead'
                elif version <= 1 and command == 'painting_time':
                    pass
                else:
                    error = 'unknown command, line ignored'

                if error:
                    errors.append((line, error))

            except Exception, e:
                errors.append((line, str(e)))
            else:
                num_found += 1
        if num_found == 0:
            errors.append(('', 'there was only garbage in this file, using defaults'))
        self.end_atomic()
        return errors

    def copy_settings_from(self, other):
        self.begin_atomic()
        for i, setting in enumerate(self.settings):
            setting.copy_from(other.settings[i])
        self.end_atomic()

    def get_color_hsv(self):
        h = self.setting_by_cname('color_h').base_value
        s = self.setting_by_cname('color_s').base_value
        v = self.setting_by_cname('color_v').base_value
        return (h, s, v)

    def set_color_hsv(self, hsv):
        self.begin_atomic()
        h, s, v = hsv
        self.setting_by_cname('color_h').set_base_value(h)
        self.setting_by_cname('color_s').set_base_value(s)
        self.setting_by_cname('color_v').set_base_value(v)
        self.end_atomic()

    def set_color_rgb(self, rgb):
        for i in range(3): assert rgb[i] <= 1.0
        self.set_color_hsv(colorsys.rgb_to_hsv(*rgb))

    def get_color_rgb(self):
        hsv = self.get_color_hsv()
        hsv = [clamp(x, 0.0, 1.0) for x in hsv]
        return colorsys.hsv_to_rgb(*hsv)

    def invert_color(self):
        rgb = self.get_color_rgb()
        rgb = [1-x for x in rgb]
        self.set_color_rgb(rgb)


class Brush(Brush_Lowlevel):
    def __init__(self, app):
        Brush_Lowlevel.__init__(self)
        self.app = app
        self.preview = None
        self.preview_thumb = None
        self.name = None
        self.preview_changed = True

        self.settings_mtime = None
        self.preview_mtime = None

    def get_fileprefix(self, saving=False):
        prefix = 'b'
        if os.path.realpath(self.app.user_brushpath) == os.path.realpath(self.app.stock_brushpath):
        #if os.path.samefile(self.app.user_brushpath, self.app.stock_brushpath):
            # working directly on brush collection, use different prefix
            prefix = 's'

        if not self.name:
            i = 0
            while 1:
                self.name = '%s%03d' % (prefix, i)
                a = os.path.join(self.app.user_brushpath,self.name + '.myb')
                b = os.path.join(self.app.stock_brushpath,self.name + '.myb')
                if not os.path.isfile(a) and not os.path.isfile(b):
                    break
                i += 1
        prefix = os.path.join(self.app.user_brushpath, self.name)
        if saving: 
            return prefix
        if not os.path.isfile(prefix + '.myb'):
            prefix = os.path.join(self.app.stock_brushpath,self.name)
        assert os.path.isfile(prefix + '.myb'), 'brush "' + self.name + '" not found'
        return prefix

    def delete_from_disk(self):
        prefix = os.path.join(self.app.user_brushpath, self.name)
        if os.path.isfile(prefix + '.myb'):
            os.remove(prefix + '_prev.png')
            os.remove(prefix + '.myb')

        prefix = os.path.join(self.app.stock_brushpath, self.name)
        if os.path.isfile(prefix + '.myb'):
            # user wants to delete a stock brush
            # cannot remove the file, manage blacklist instead
            filename = os.path.join(self.app.user_brushpath, 'deleted.conf')
            new = not os.path.isfile(filename)
            f = open(filename, 'a')
            if new:
                f.write('# list of stock brush names which you have deleted\n')
                f.write('# you can remove this file to get all of them back\n')
            f.write(self.name + '\n')
            f.close()

    def remember_mtimes(self):
        prefix = self.get_fileprefix()
        self.preview_mtime = os.path.getmtime(prefix + '_prev.png')
        self.settings_mtime = os.path.getmtime(prefix + '.myb')

    def has_changed_on_disk(self):
        prefix = self.get_fileprefix()
        if self.preview_mtime != os.path.getmtime(prefix + '_prev.png'): return True
        if self.settings_mtime != os.path.getmtime(prefix + '.myb'): return True
        return False

    def save(self):
        prefix = self.get_fileprefix(saving=True)
        if self.preview_changed:
            self.preview.save(prefix + '_prev.png', 'png')
            self.preview_changed = False
        open(prefix + '.myb', 'w').write(self.save_to_string())
        self.remember_mtimes()

    def load(self, name):
        self.name = name
        prefix = self.get_fileprefix()

        filename = prefix + '_prev.png'
        pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
        self.update_preview(pixbuf)

        if prefix.startswith(self.app.user_brushpath):
            self.preview_changed = False
        else:
            # for saving, create the preview file even if not changed
            self.preview_changed = True

        filename = prefix + '.myb'
        errors = self.load_from_string(open(filename).read())
        if errors:
            print '%s:' % filename
            for line, reason in errors:
                print line
                print '==>', reason
            print

        self.remember_mtimes()

    def reload_if_changed(self):
        if self.settings_mtime is None: return
        if self.preview_mtime is None: return
        if not self.name: return
        prefix = self.get_fileprefix()
        if not self.has_changed_on_disk(): return False
        print 'Brush "' + self.name + '" has changed on disk, reloading it.'
        self.load(self.name)
        return True

    def update_preview(self, pixbuf):
        self.preview = pixbuf
        self.preview_thumb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, thumb_w, thumb_h)
        pixbuf_scale_nostretch_centered(src=pixbuf, dst=self.preview_thumb)
        self.preview_changed = True

DrawWidget = mydrawwidget.MyDrawWidget
