#!/usr/bin/python3
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""
  Yaroslav Halchenko
  web:     http://www.onerussian.com
  e-mail:  yoh@onerussian.com

 DESCRIPTION (NOTES):

    See README.rst shipped with this tool.
    "Homepage" for the tool is http://github.com/yarikoptic/svgtune

 LICENSE:

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the
  Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
  MA 02110-1301, USA.

 On Debian system see /usr/share/common-licenses/GPL for the full license.
"""
from __future__ import print_function

__author__ = 'Yaroslav Halchenko'
__version__ = (0, 3, 1)
__copyright__ = 'Copyright (c) 2009-2020 Yaroslav Halchenko'
__license__ = 'GPL'

import os, re
from lxml import etree
from optparse import OptionParser

# Setup rudimentary logging
import logging
lgr = logging.getLogger("svgtune")
lgr.addHandler(logging.StreamHandler())


def split2(l, msg, sep=' '):
    """Split l into 2 pieces using ' ' as a separator
    """
    p = [x.strip() for x in (l.split(sep, 1)) if x != '']

    if len(p) != 2:
        raise ValueError(msg + '. Got %s for "%s"' % (p, l))

    return p


def replace_key(attr_value, key, value):
    """Replace value for a given key

       If key is not found, new one is added
    """
    v = attr_value.split(';')
    changed = False
    newval = '%s:%s' % (key, value)
    for i in range(len(v)):
        if v[i].startswith(key + ':'):
            v[i] = newval
            changed = True
    if not changed:
        # Wasn't found -- add one
        v += [newval]
    return ';'.join(x for x in v if x.strip())


def change_attr(el, attr, values):
    """Change values listed within attribute attr
    """
    v = el.attrib.get(attr, '')
    changed = False
    for value in values.split(';'):
        k, newv = split2(value, "Each value must be in the form x:y", ":")
        v = replace_key(v, k, newv)
    if v == '':                         # there were no such yet
        v = "%s:%s" % (k, newv)
    #print "Changing %s : %s, got %s" % (attr, values, str(v))
    el.attrib[attr] = v


def verbose(level, str_):
    if level <= options.verbose:
        lgr.info(" "*(level-1) + str_)


def version():
    print("svgtune v" + ".".join(__version__))
    raise SystemExit


def process_options(params, options):
    # For now simply split by space
    option_values = params.split(' ')
    for option_value in option_values:
        t = option_value.split('=', 1)
        option = t[0]
        value = t[1] if len(t) > 1 else None
        if option in ["previews", "nosvgs"]:
            # if only said "previews" assume that we want them
            if value is None:
                value = True
            setattr(options, option, bool(value))
        else:
            raise ValueError("Unknown option %s" % option)
    pass


def load_svg(svgfile):
    verbose(1, "Processing file %s for tuning" % svgfile)
    svgdoc = etree.parse(svgfile)
    svg = svgdoc.getroot()

    dname = '%s_tuned' % os.path.splitext(svgfile)[0]
    try:
        os.mkdir(dname)
    except:
        pass # if directory exists or hell with everything
    return svg, svgdoc, dname


def parse_cmdline():
    parser = OptionParser(usage="%prog [options] inputfile.svgtune",
                          version="%prog version "
                                  + ".".join([str(i) for i in __version__]))
    parser.add_option("-v", action="count", dest="verbose",
                      help="Increase verbosity with multiple -v", default=0)
    parser.add_option("-p", "--previews", action="store_true",
                      dest="previews", help="Store preview png's")
    parser.add_option("--no-svgs", action="store_true",
                      dest="nosvgs", help="Remove 'final' svgs")


    (options_, args) = parser.parse_args()

    if len(args) != 1:
        raise SystemExit(
            "Error: Please provide single input file with instructions.")

    lgr.setLevel(logging.DEBUG if options_.verbose >= 4 else logging.INFO)

    return args[0], options_


def svgtune(ifile, options):
    svg = None
    dname = None

    for line_ in open(ifile).readlines():
        line = line_.strip()
        if line.startswith('#') or line == '':
            continue
        if line.rstrip() == '%exit':
            break

        # parse out first element
        cmd, params = split2(line, "Each line must be 'command parameters'")

        if cmd == '%file':
            svg, svgdoc, dname = load_svg(params)
            continue
        elif cmd == "%options":
            process_options(params, options)
            continue

        # We must have file loaded by now
        if svg is None:
            svgfile = os.path.splitext(ifile)[0] + '.svg'
            try:
                svg, svgdoc, dname = load_svg(svgfile)
            except Exception as e:
                raise RuntimeError(
                    "Tried to load from %s but failed due to %s.\n"
                    "Please provide %%file directive to load the file prior %s "
                    "or have .svg file with the same name as .svgtune."
                    % (svgfile, e, cmd))

        elif cmd == '%save':
            ofile = '%s/%s.svg' % (dname, params)
            verbose(1, "Storing into %s" % ofile)
            with open(ofile, 'wb') as f:
                f.write(etree.tostring(svgdoc, pretty_print=True))
            if options.previews:
                verbose(3, "Generating preview")
                os.system('inkscape -z -f %s -e %s -d 300 >/dev/null 2>&1' %
                          (ofile, ofile.replace('.svg', '_preview.png')))
            if options.nosvgs:
                verbose(4, "Removing generated svg")
                os.remove(ofile)
            continue

        #
        # Figure out the victims for changes -- victims
        victims = None
        if cmd == 'layers':
            changes = params
            victims = svg.findall('.//{%s}g[@{%s}groupmode="layer"]'
                                  % (svg.nsmap['svg'], svg.nsmap['inkscape']))
        elif cmd in ['layer', 'g', 'text', 'any']:
            # parse out first element
            identifier, changes = split2(params,
                       "For each layer or g you must list id or label + changes")

            # determine the victims
            sid = ''
            id1, id2 = identifier.split('=')
            sid_re_attr = None             # either we need to do re.search
            sid_re_str = id2

            if cmd == 'layer':
                sid += '[@{%s}groupmode="layer"]' % svg.nsmap['inkscape']
                elem = 'g'
            elif cmd == 'any':
                # will be custom anyways
                pass
            else:
                elem = cmd


            if id1 == 'label':
                sid += '[@{%s}label="%s"]' % (svg.nsmap['inkscape'], id2)
            elif id1 == 'id':
                sid += '[@id="%s"]' % (id2,)
            elif id1 == 'href':
                sid += '[@{%s}href="%s"]' % (svg.nsmap['xlink'], id2)
            elif id1 == 'href:re':
                sid += '{%s}href' % svg.nsmap['xlink']
                lgr.debug(svg.nsmap['xlink'])
                lgr.debug("sid=%r" % sid)
            elif id1 == 'label:re':
                sid_re_attr = '{%s}label' % svg.nsmap['inkscape']
            elif id1 == 'id:re':
                sid_re_attr = 'id'
            else:
                raise ValueError("Unknown identifier %s in %s" % (id1, line))

            if cmd != 'any':
                victims = svg.findall('.//{%s}%s%s'
                                      % (svg.nsmap['svg'], elem, sid))
                lgr.debug("Found %d victims searching for %r" % (len(victims), sid))
            else:
                victims = svg.findall('.//')
                lgr.debug("Found %d overall victims" % len(victims))
                if not id1.endswith(':re'):
                    victims = [v for v in victims if v.attrib.get(id1, None) == id2]
                    lgr.debug("After selection based on %s was left with "
                              "%d victims" % (id1, len(victims)))

            # optionally perform search using re
            if sid_re_attr is not None:
                regexp = re.compile(sid_re_str)
                victims_orig = victims # for debugging
                victims = [v for v in victims
                           if regexp.search(v.attrib.get(sid_re_attr, ""))]
                lgr.debug("Left with %d after matching regexp %r"
                          % (len(victims), sid_re_str))

            nvictims = len(victims)
            if nvictims == 0:
                raise ValueError("Cannot find any victim for '%s'" % identifier)
            elif nvictims > 1 and not sid_re_attr:
                raise ValueError(
                    "We should get a single victim for %s "
                    "but got %d of them" % (identifier, nvictims))
        else:
            raise ValueError("Unknown command '%s'." % cmd)

        #
        # Figure out the actual changes to take and do them
        for change in changes.split(' '):
            if change == '':
                continue
            attr, values = split2(
                change,
                'Each change should be listed as attribute=value',
                '=')
            if attr == 'style':
                func = lambda vic: change_attr(vic, attr, values)
            else:
                raise ValueError("Don't yet handle attribute %s" % attr)

            for victim in victims:
                func(victim)


if __name__ == "__main__":
    ifile, options = parse_cmdline()
    svgtune(ifile, options)
