#!/usr/bin/env python

"""Slicer tool

    For more information run: slicer --help

    Author: Stefan Urbanek <stefan.urbanek@gmail.com>
    Date: 2011-01
"""

import json
import argparse
import sys
import cubes
import cubes.server
import ConfigParser
import shlex
import os

from collections import OrderedDict

from cubes.common import MissingPackageError
from cubes.logging import create_logger
from cubes.errors import CubesError
from cubes.metadata import read_model_metadata, write_model_metadata_bundle

try:
    from cubes_modeler import ModelEditorSlicerCommand
except ImportError:
    ModelEditorSlicerCommand = None

def validate_model(args):
    """docstring for validate_model"""
    print("Reading model %s" % args.model)
    model = cubes.read_model_metadata(args.model)

    print("Validating model...\n")
    result = cubes.providers.validate_model(model)

    error_count = 0
    warning_count = 0
    default_count = 0

    for error in result:
        if error.scope == "model":
            scope = "model"
        else:
            if error.object:
                scope = "%s '%s'" % (error.scope, error.object)
            else:
                scope = "unknown %s" % error.scope

        if error.property:
            scope += " property '%s'" % error.property

        show = True
        if error.severity == "error":
            error_count += 1
        elif error.severity == "warning":
            warning_count += 1
            show = args.show_warnings
        elif error.severity == "default":
            show = args.show_defaults
            default_count += 1

        if show:
            print "%s in %s: %s" % (error.severity.upper(), scope, error.message)

    if error_count == 0:
        if warning_count == 0:
            if default_count == 0:
                verdict = "model can be used"
            else:
                verdict = "model can be used, " \
                          "make sure that the defaults reflect reality"
        else:
            verdict = "not recommended to use the model, " \
                      "some issues might emerge"
    else:
        verdict = "model can not be used"

    print("")
    print("Defaults used  %d" % default_count)
    print("Warning        %d" % warning_count)
    print("Errors         %d" % error_count)
    print("Summary        %s" % verdict)

    if error_count > 0:
        exit(1)

def convert_model(args):
    raise NotImplementedError("Temporarily disabled.")

    path = args.target

    workspace = Workspace()
    for model in args.models:
        workspace.add_model(model)

    if args.format == "bundle":
        if os.path.exists(path):
            if not os.path.isdir(path):
                raise CubesError("Target exists and is a file, "
                                 "can not replace")
            elif not os.path.exists(os.path.join(path, "model.json")):
                raise CubesError("Target is not a model directory, "
                                    "can not replace.")
            if args.force:
                shutil.rmtree(path)
            else:
                raise CubesError("Target already exists. "
                                    "Remove it or use --force.")
        cubes.write_model_bundle(model, args.target)

    elif args.format == "json":
        info = model.to_dict(target="origin")
        if not path:
            print json.dumps(info)
        else:
            with open(path, "w") as f:
                json.dump(info, f)

def merge_model(args):
    # For more information, read Workspace.add_model() and Workspace.model()

    master_model = OrderedDict()
    master_model["__comment"] = "This is a master model. Do not edit."
    master_model["__is_master"] = True

    models = []
    for model in args.models:
        models.append(read_model_metadata(model))

    master_model["parts"] = models

    if not args.target:
        print json.dumps(master_model)
    else:
        with open(args.target, "w") as f:
            json.dump(master_model, f)

def model_to_json(args):
    raise NotImplementedError("Temporarily disabled.")

def update_locale(args):
    raise NotImplementedError("update of localizable dictionary is not yet implemented")

def extract_locale(args):
    raise NotImplementedError("Temporarily disabled.")
    model = cubes.model_from_path(args.model)
    print json.dumps(model.localizable_dictionary())

def translate_model(args):
    raise NotImplementedError("Temporarily disabled.")
    model = cubes.model_from_path(args.model)
    trans_path = args.translation

    with open(trans_path) as f:
        trans_dict = json.load(f)

    model = model.localize(trans_dict)
    dump_model(model)

def dump_model(model):
    print json.dumps(model.to_dict(with_mappings=True))

def read_config(cfg):
    """Read the configuration file."""
    config = ConfigParser.SafeConfigParser()
    try:
        config.read(args.config)
    except Exception as e:
        raise Exception("Unable to load config: %s" % e)

    return config

def generate_ddl(args):
    raise NotImplementedError("Temporarily disabled.")
    model = cubes.load_model(args.model)

    if args.backend:
        backend = cubes.workspace.get_backend(args.backend)
    else:
        backend = cubes.backends.sql.browser

    ddl = backend.ddl_for_model(args.url, model, fact_prefix=args.fact_prefix,
                                dimension_prefix=args.dimension_prefix,
                                fact_suffix=args.fact_suffix,
                                dimension_suffix=args.dimension_suffix)

    print ddl

def run_server(args):
    """Run Slicer HTTP server."""
    config = read_config(args.config)

    # Load extensions

    if config.has_option("server", "modules"):
        modules = shlex.split(config.get("server", "modules"))
        for module in modules:
            e = __import__(module)

    if config.has_option("server", "pid_file"):
        path = config.get("server", "pid_file")
        try:
            with open(path, "w") as f:
                f.write("%s\n" % os.getpid())
        except IOError:
            raise CubesError("Unable to write PID file '%s'. Check the "
                             "directory existence or permissions." % path)

    cubes.server.run_server(config, debug=args.debug)

def run_test(args):
    """Run test of Slicer HTTP server configuration."""
    raise NotImplementedError("Temporarily disabled.")
    config = read_config(args.config)
    workspace = Workspace(config)

    results = workspace.validate_model()
    if results:
        print("\nvalidation results:\n")
        for result in results:
            print("%s: %s" % (result[0], result[1]) )
    else:
        print("model test was successful")

def denormalize(args):
    raise NotImplementedError("Temporarily disabled.")
    cube_list = args.cube
    config = read_config(args.config)

    workspace = Workspace(config)

    model = workspace.model

    if not cube_list:
        cube_list = [cube.name for cube in model.cubes.values()]

    view_schema = args.schema # or workspace.options.get("denormalized_view_schema")
    view_prefix = args.prefix or workspace.options.get("denormalized_view_prefix")

    for cube_name in cube_list:
        cube = model.cube(cube_name)

        view_name = view_prefix + cube_name if view_prefix else cube_name

        print("denormalizing cube '%s' into '%s'" % (cube_name, view_name))

        workspace.create_denormalized_view(cube, view_name,
                                            materialize=args.materialize,
                                            replace=args.replace,
                                            create_index=args.index,
                                            keys_only=False,
                                            schema=view_schema)

def convert_model(args):
    path = args.target
    model = read_model_metadata(args.model)

    if args.format == "json":
        if not path:
            print json.dumps(model, indent=4)
        else:
            with open(path, "w") as f:
                json.dump(model, f, indent=4)
    elif args.format == "bundle":
        write_model_metadata_bundle(path, model, replace=args.force)

def run_test(args):
    """Run test of Slicer HTTP server configuration."""
    raise NotImplementedError("Temporarily disabled.")
    config = read_config(args.config)
    workspace = Workspace(config)

    results = workspace.validate_model()
    if results:
        print("\nvalidation results:\n")
        for result in results:
            print("%s: %s" % (result[0], result[1]) )
    else:
        print("model test was successful")

def denormalize(args):
    raise NotImplementedError("Temporarily disabled.")
    cube_list = args.cube
    config = read_config(args.config)

    workspace = Workspace(config)

    model = workspace.model

    if not cube_list:
        cube_list = [cube.name for cube in model.cubes.values()]

    view_schema = args.schema # or workspace.options.get("denormalized_view_schema")
    view_prefix = args.prefix or workspace.options.get("denormalized_view_prefix")

    for cube_name in cube_list:
        cube = model.cube(cube_name)

        view_name = view_prefix + cube_name if view_prefix else cube_name

        print("denormalizing cube '%s' into '%s'" % (cube_name, view_name))

        workspace.create_denormalized_view(cube, view_name,
                                            materialize=args.materialize,
                                            replace=args.replace,
                                            create_index=args.index,
                                            keys_only=False,
                                            schema=view_schema)

def convert_model(args):
    path = args.target
    model = read_model_metadata(args.model)

    if args.format == "json":
        if not path:
            print json.dumps(model, indent=4)
        else:
            with open(path, "w") as f:
                json.dump(model, f, indent=4)
    elif args.format == "bundle":
        write_model_metadata_bundle(path, model, replace=args.force)


def edit_model(args):
    if not run_modeler:
        sys.stderr.write("ERROR: 'cubes_modeler' package needs to be "
                         "installed to edit the model.\n")
        exit(1)

    if args.port:
        port = int(args.port)
    else:
        port = 5000

    import webbrowser
    webbrowser.open("http://127.0.0.1:%s" % port)

    run_modeler(args.model, args.target)

################################################################################
# Main code

parser = argparse.ArgumentParser(description='Cubes tool')
subparsers = parser.add_subparsers(title='commands')
parser.add_argument('--cubes-debug',
                    dest='cubes_debug', action='store_true', default=False,
                            help='internal cubes debugging')

################################################################################
# Command: valdate_model

model_parser = subparsers.add_parser('model', help="logical model validation, translation, conversion")
model_subparsers = model_parser.add_subparsers(title='model commands',
                            help='additional model help')

parser_validate = model_subparsers.add_parser('validate',
                            help="validate model and print validation report")

parser_validate.add_argument('-d', '--defaults',
                            dest='show_defaults', action='store_true', default=False,
                            help='show defaults')
parser_validate.add_argument('--no-warnings',
                            dest='show_warnings', action='store_false', default=True,
                            help='disable warnings')

parser_validate.add_argument('model', help='model reference - can be a local file path or URL')
parser_validate.set_defaults(func=validate_model)


################################################################################
# Command: edit

if ModelEditorSlicerCommand:
    subparser = model_subparsers.add_parser("edit", help="edit model")

    command = ModelEditorSlicerCommand()
    command.configure_parser(subparser)
    subparser.det_defaults(func=command)

################################################################################
# Command: translate_model

subparser = model_subparsers.add_parser('translate', help="translate model")
subparser.add_argument('model', help='model file or URL')
subparser.add_argument('translation', help='translation file or URL')
subparser.set_defaults(func=translate_model)

################################################################################
# Command: model_to_json

subparser = model_subparsers.add_parser('json')
subparser.add_argument('models', nargs='+', help='one or more model references - can be local file paths or URL')
subparser.add_argument('target', nargs='?', help='optional target path to write json model to')
subparser.set_defaults(func=merge_model)

################################################################################
# Command: merge

subparser = model_subparsers.add_parser('merge')
subparser.add_argument('models', nargs='+', help='one or more model references - can be local file paths or URL')
subparser.add_argument('target', help='target output path', nargs='?', default=None)
subparser.set_defaults(func=merge_model)

################################################################################
# Command: convert

subparser = model_subparsers.add_parser('convert')
subparser.add_argument('--format',
                            dest='format',
                            choices=('json', 'bundle'),
                            default='json',
                            help='output model format')
subparser.add_argument('--force',
                            dest='force', action='store_true', default=False,
                            help='replace existing model bundle')
subparser.add_argument('model', help='model reference - can be a path or URL')
subparser.add_argument('target', help='target output path', nargs='?', default=None)
subparser.set_defaults(func=convert_model)

################################################################################
# Command: extract_locale

subparser = model_subparsers.add_parser('extract_locale', help="extract model localization dictionary")
subparser.add_argument('model', help='model reference - can be a local file path or URL')
subparser.set_defaults(func=extract_locale)

################################################################################
# Command: update_locale

subparser = model_subparsers.add_parser('update_locale', help="update model localization dictionary")
subparser.add_argument('model', help='model reference - can be a local file path or URL')
subparser.add_argument('translation', help='translation file or URL')
subparser.set_defaults(func=update_locale)

################################################################################
# Command: serve

subparser = subparsers.add_parser('serve', help="run slicer server")
subparser.add_argument('config', help='server confuguration .ini file')
subparser.set_defaults(func=run_server)

subparser.add_argument('--debug',
                            dest='debug', action='store_true', default=False,
                            help="Run the server in debug mode")

################################################################################
# Command: serve

subparser = subparsers.add_parser('test', help="test the configuration and model with backend")
subparser.add_argument('config', help='server confuguration .ini file')
subparser.set_defaults(func=run_test)

################################################################################
# Command: denormalize

subparser = subparsers.add_parser('denormalize',
                                  help="create denormalized view(s) using SQL star backend")
subparser.add_argument('config', help='slicer confuguration .ini file')
subparser.add_argument('-p', '--prefix',
                            dest='prefix',
                            help='prefix for denormalized views (overrides config value)')
subparser.add_argument('-f', '--force',
                            dest='replace', action='store_true', default=False,
                            help='replace existing views')
subparser.add_argument('-m', '--materialize',
                            dest='materialize', action='store_true', default=False,
                            help='create materialized view (table)')
subparser.add_argument('-i', '--index',
                            dest='index', action='store_true', default=False,
                            help='create index for key attributes')
subparser.add_argument('-s', '--schema',
                            dest='schema',
                            help='target view schema (overrides config value)')
subparser.add_argument('-c', '--cube',
                            dest='cube', action='append',
                            help='cube(s) to be denormalized, if not specified then all in the model')
subparser.set_defaults(func=denormalize)

################################################################################
# Command: ddl

subparser = subparsers.add_parser('ddl', help="generate DDL of star schema, based on logical model (works only for SQL backend)")
subparser.add_argument('url', help='SQL database connection URL')
subparser.add_argument('model', help='model reference - can be a local file path or URL')
subparser.add_argument('--dimension-prefix',
                            dest='dimension_prefix',
                            help='prefix for dimension tables')
subparser.add_argument('--fact-prefix',
                            dest='fact_prefix',
                            default="",
                            help='prefix for fact tables')
subparser.add_argument('--dimension-suffix',
                       dest='dimension_suffix',
                       help='suffix for dimension tables')
subparser.add_argument('--fact-suffix',
                       dest='fact_suffix',
                       default="",
                       help='suffix for fact tables')
subparser.add_argument('--backend',
                            dest='backend',
                            help='backend name (currently limited only to SQL backends)')
subparser.set_defaults(func=generate_ddl)

args = parser.parse_args(sys.argv[1:])

if args.cubes_debug:
    args.func(args)
else:
    try:
        args.func(args)
    except CubesError as e:
        sys.stderr.write("ERROR: %s\n" % e)
        exit(1)
    except MissingPackageError as e:
        sys.stderr.write("MISSING PACKAGE ERROR: %s\n" % e)
        exit(2)

