#!/usr/bin/env python
#   vim: fileencoding=utf-8

# This file is part of open-hea.
# 
# open-hea 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 3 of the License, or (at your
# option) any later version.
# 
# open-hea 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 open-hea.  If not, see <http://www.gnu.org/licenses/>.


__author__ = "Dave Haynes"
__date__ = "September 2012"
__contact__ = "tundish@thuswise.org"
__doc__ = """
This script compares the SQLAlchemy-generated model with a reference
schema. The result is a side-by-side diff in HTML format.

Example::

    open-hea$ python scripts/schema-diff.py \
    < src/openhea/model/scripts/openheadb_mysql.sql \
    > schema-diff.html

"""

import argparse
from cStringIO import StringIO
import difflib
import sys

import sqlalchemy

from pygments.filter import Filter
from pygments.lexers import get_lexer_by_name
from pygments.token import Token

TTW = Token.Text.Whitespace
TLSS = Token.Literal.String.Symbol 

from openhea.model.unstableschema import metadata
from openhea.model.unstableschema import declaration_ordering

class ProvokeSQLAQuoting(Filter):
    """
    SQLAlchemy by default does not quote names, which makes difficult
    any comparison with the output of `mysqldump` (where all names are
    quoted). We can provoke quoting by making sure there is at least one
    upper case character in the name. The documented behaviour of SQLA
    is then to quote the string.

    This class is an SQLA filter which provides a means of mutating the
    name to one which contains an upper case character, and which
    translates it back again when delegated by the lexer to operate on the
    token stream.
    
    """

    lookup = {}

    @staticmethod
    def mutate(val):
        rv = val.capitalize()
        ProvokeSQLAQuoting.lookup[rv] = val
        return rv

    def filter(self, lexer, stream):
        for ttype, value in stream:
            if (ttype is TLSS and
                value.startswith('`') and value.endswith('`')):
                name = value[1:-1]
                if name in ProvokeSQLAQuoting.lookup:
                    value = u"`{0}`".format(ProvokeSQLAQuoting.lookup[name])
            yield ttype, value

class SQLAFormatter(object):
    """
    This class does what's necessary to coaxe sqlalchemy into generating
    the MySQL schema from our model, in the order we want the tables
    to appear.
    """
    def __init__(self, md):
        self.md = md
        self.stream = StringIO()

        self.engine = sqlalchemy.create_engine(
            "mysql://", strategy="mock", use_ansiquotes=True,
            executor=self.dump)

    def dump(self, sql, *args, **kwargs):
        dialect = self.engine.dialect
        self.stream.write(str(sql.compile(dialect=dialect)))

    def output(self):
        for k in declaration_ordering:
            t = k.__table__
            t.name = ProvokeSQLAQuoting.mutate(t.name)
            for col in t.c:
                col.name = ProvokeSQLAQuoting.mutate(col.name)
            t.create(self.engine, checkfirst=True)

        return self.stream.getvalue()

def main(args):
    """
    Set up a lexer to tokenise MySQL-style statements. Compare the input
    from stdin (a reference) with what sqlalchemy makes of our
    declarative model.
    """
    lxr = get_lexer_by_name("mysql", encoding="utf-8", stripall="True")
    lxr.add_filter("tokenmerge")
    lxr.add_filter("whitespace", tabs=" ", tabsize=2)
    lxr.add_filter(ProvokeSQLAQuoting())
    fmttr = SQLAFormatter(metadata)

    input_ = sys.stdin.read()
    model_sql = fmttr.output()

    ref_toks = [str(i) for i in lxr.get_tokens(input_) if i[0] is not TTW]
    model_out = list(lxr.get_tokens(model_sql))
    model_toks = [str(i) for i in model_out if i[0] is not TTW]
    output = difflib.HtmlDiff()
    sys.stdout.write(
        output.make_file(model_toks, ref_toks, "SQLA Model", "MySQL Reference"))
    if args.debug:
        sys.stderr.write(''.join(i[1] for i in model_out))
        sys.stderr.write("\n")
    return 0

def parser():
    rv = argparse.ArgumentParser(
            description=__doc__,
            formatter_class=argparse.RawDescriptionHelpFormatter)
    rv.add_argument("-d", "--debug", action="store_true", default=False,
        help="add debugging information (reports model schema)")
    return rv

def run():
    p = parser()
    args = p.parse_args()
    rv = main(args)
    sys.exit(rv)

if __name__ == "__main__":
    run()
