namespace SQLHeavy {
  public errordomain GeneratorError {
    CONFIGURATION,
    METADATA,
    SYMBOL_RESOLVER,
    DATABASE,
    SELECTOR
  }

  public class Generator : GLib.Object {
    [CCode (array_length = false, array_null_terminated = true)]
    static string[] sources;
    [CCode (array_length = false, array_null_terminated = true)]
    static string[] vapi_directories;
    [CCode (array_length = false, array_null_terminated = true)]
    static string[] packages;
    static string metadata_location;
    static string output_location;
    static bool write_properties;

    private Vala.CodeContext context = new Vala.CodeContext ();
    private GLib.SList<string> databases = new GLib.SList<string> ();

    const GLib.OptionEntry[] options = {
      { "metadata", 'm', 0, OptionArg.FILENAME, ref metadata_location, "Load metadata from FILE", "FILE..." },
      { "vapidir", 0, 0, OptionArg.FILENAME_ARRAY, ref vapi_directories, "Look for package bindings in DIRECTORY", "DIRECTORY..." },
      { "pkg", 0, 0, OptionArg.STRING_ARRAY, ref packages, "Include binding for PACKAGE", "PACKAGE..." },
      { "output", 'o', 0, OptionArg.FILENAME, ref output_location, "Output to FILE", "FILE..." },
      { "properties", 'p', 0, GLib.OptionArg.NONE, ref write_properties, "Write properties instead of methods", null },
      { "", 0, 0, OptionArg.FILENAME_ARRAY, ref sources, "SQLite databases", "DATABASE..." },
      { null }
    };

    private Vala.HashMap<string, Vala.HashMap <string, string>> cache =
      new Vala.HashMap<string, Vala.HashMap <string, string>> (GLib.str_hash, GLib.str_equal);
    private Vala.HashMap<string, Vala.HashMap <string, string>> wildcard_cache =
      new Vala.HashMap<string, Vala.HashMap <string, string>> (GLib.str_hash, GLib.str_equal);

    private Vala.HashMap <string, string> get_symbol_properties (string symbol) {
      var map = this.cache.get (symbol);
      if ( map != null )
        return map;

      map = new Vala.HashMap<string,string> (GLib.str_hash, GLib.str_equal, GLib.str_equal);
      foreach ( string selector in this.wildcard_cache.get_keys () ) {
        if ( GLib.PatternSpec.match_simple (selector, symbol) ) {
          var wmap = this.wildcard_cache.get (selector);
          foreach ( string key in wmap.get_keys () )
            map.set (key, wmap.get (key));
        }
      }

      this.cache.set (symbol, map);
      return map;
    }

    private void set_symbol_property (string symbol, string key, string value) {
      this.get_symbol_properties (symbol).set (key, value);
    }

    private string? get_symbol_property (string symbol, string key) {
      return this.get_symbol_properties (symbol).get (key);
    }

    private string get_symbol_name (string symbol) {
      string? sym = this.get_symbol_property (symbol, "name");
      if ( sym != null )
        return sym;

      int sym_t = 3;
      bool tb = true, sb = true, tf = true;
      GLib.StringBuilder name = new GLib.StringBuilder.sized (symbol.length * 2);
      for ( sym = symbol ; ; sym = sym.offset (1) ) {
        var c = sym.get_char_validated ();
        if ( c <= 0 )
          break;

        if ( sb ) {
          if ( c == '@' ) {
            sym_t = 1;
            continue;
          } else if ( c == '%' ) {
            sym_t = 2;
            continue;
          }
        }

        if ( c == '_' ) {
          tb = true;
          tf = true;
          continue;
        } else if ( c == '/' ) {
          sym_t = int.min (3, sym_t + 1);
          tf = tb = sb = true;
          name.truncate (0);
          continue;
        }

        if ( c.isupper () && !tb ) {
          if ( sym_t == 3 )
            name.append_c ('_');
          tb = true;
          tf = false;
          name.append_unichar (sym_t == 3 ? c.tolower () : c.toupper ());
          continue;
        } else if ( c.islower () && tb ) {
          if ( tf && sym_t != 3 )
            name.append_unichar (c.toupper ());
          else if ( tf && !sb && sym_t == 3 ) {
            name.append_c ('_');
            name.append_unichar (c);
          }
          else
            name.append_unichar (c);
          tb = tf = false;
          continue;
        }

        sb = false;
        name.append_unichar (tb ? (sym_t == 3 ? c.tolower () : c.toupper ()) : c.tolower ());
        tf = false;
      }

      this.set_symbol_property (symbol, "name", name.str);
      return name.str;
    }

    public bool symbol_is_hidden (string symbol) {
      var p = this.get_symbol_property (symbol, "hidden");
      return p != null && (p == "1" || p == "true" || p == "yes");
    }

    private static Vala.DataType type_from_string (string datatype) {
      bool is_array = false;
      var internal_datatype = datatype;
      Vala.UnresolvedSymbol? symbol = null;

      if ( datatype.has_suffix ("[]") ) {
        internal_datatype = internal_datatype.substring (0, -2);
        is_array = true;
      }

      foreach ( unowned string m in internal_datatype.split (".") )
        symbol = new Vala.UnresolvedSymbol (symbol, m);

      var data_type = new Vala.UnresolvedType.from_symbol (symbol);
      if ( is_array )
        return new Vala.ArrayType (data_type, 1, null);
      else
        return data_type;
    }

    private Vala.DataType? get_data_type (string symbol) {
      string? name = this.get_symbol_property (symbol, "type");

      return name == null ? null : type_from_string (name);
    }

    private void parse_field (SQLHeavy.Table table, int field, Vala.Class cl, Vala.SwitchStatement signals, Vala.SourceReference source_reference) throws GeneratorError, SQLHeavy.Error {
      var db = table.queryable.database;
      var db_symbol = GLib.Path.get_basename (db.filename).split (".", 2)[0];
      var symbol = @"@$(GLib.Path.get_basename (db_symbol))/$(table.name)/$(table.field_name (field))";
      var name = this.get_symbol_name (symbol);

      if ( this.symbol_is_hidden (symbol) )
        return;

      var data_type = this.get_data_type (symbol);
      if ( data_type == null ) {
        var affinity = table.field_affinity (field).down ().split (" ");

        if ( affinity[0] == "integer" )
          affinity[0] = "int";
        else if ( affinity[0] == "text" ||
                  affinity[0].has_prefix ("varchar") ||
                  affinity[0].has_prefix ("char") )
          affinity[0] = "string";
        else if ( affinity[0] == "blob" )
          affinity[0] = "uint8[]";
        else if ( affinity[0] == "timestamp" ||
                  affinity[0] == "datetime" )
          affinity[0] = "time_t";

        data_type = type_from_string (affinity[0]);
      }

      var data_type_get = data_type.copy ();
      data_type_get.value_owned = true;

      var switch_section = new Vala.SwitchSection (source_reference);
      signals.add_section (switch_section);
      switch_section.add_label (new Vala.SwitchLabel (new Vala.StringLiteral (@"\"$(name)\"", source_reference), source_reference));
      Vala.MethodCall emit_changed_notify;

      if ( !write_properties ) {
        var changed_signal = new Vala.Signal (@"$(name)_changed", new Vala.VoidType (source_reference), source_reference);
        changed_signal.access = Vala.SymbolAccessibility.PUBLIC;
        cl.add_signal (changed_signal);
        emit_changed_notify = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this"), @"$(name)_changed", source_reference), source_reference);

        {
          var get_method = new Vala.Method (@"get_$(name)", data_type_get, source_reference);
          cl.add_method (get_method);
          get_method.access = Vala.SymbolAccessibility.PUBLIC;
          get_method.add_error_type (type_from_string ("SQLHeavy.Error"));

          var block = new Vala.Block (source_reference);
          var call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this"), @"get_$(data_type_get.to_string ())", source_reference), source_reference);
          call.add_argument (new Vala.StringLiteral (@"\"$(table.field_name (field))\"", source_reference));
          block.add_statement (new Vala.ReturnStatement (call, source_reference));

          get_method.body = block;
        }

        {
          var set_method = new Vala.Method (@"set_$(name)", new Vala.VoidType (source_reference), source_reference);
          set_method.add_parameter (new Vala.Parameter ("value", data_type, source_reference));
          cl.add_method (set_method);
          set_method.access = Vala.SymbolAccessibility.PUBLIC;
          set_method.add_error_type (type_from_string ("SQLHeavy.Error"));

          var block = new Vala.Block (source_reference);
          var call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this"), @"set_$(data_type.to_string ())", source_reference), source_reference);
          call.add_argument (new Vala.StringLiteral (@"\"$(table.field_name (field))\"", source_reference));
          block.add_statement (new Vala.ExpressionStatement (call, source_reference));

          set_method.body = block;
        }
      } else {
        Vala.PropertyAccessor get_accessor, set_accessor;
        emit_changed_notify = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this", source_reference), "notify_property", source_reference), source_reference);
        emit_changed_notify.add_argument (new Vala.StringLiteral ("\"" + name.replace ("_", "-") + "\"", source_reference));
        {
          var block = new Vala.Block (source_reference);
          var try_block = new Vala.Block (source_reference);
          var catch_block = new Vala.Block (source_reference);
          var try_stmt = new Vala.TryStatement (try_block, null, source_reference);

          var call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this"), @"get_$(data_type_get.to_string ())", source_reference), source_reference);
          call.add_argument (new Vala.StringLiteral (@"\"$(table.field_name (field))\"", source_reference));
          try_block.add_statement (new Vala.ReturnStatement (call, source_reference));

          var error_call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("GLib"), "error", source_reference), source_reference);
          error_call.add_argument (new Vala.StringLiteral (@"\"Unable to retrieve `$(name)': %s\"", source_reference));
          error_call.add_argument (new Vala.MemberAccess (new Vala.MemberAccess (null, "e"), "message", source_reference));
          catch_block.add_statement (new Vala.ExpressionStatement (error_call, source_reference));

          var anr = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("GLib", source_reference), "assert_not_reached", source_reference));
          catch_block.add_statement (new Vala.ExpressionStatement (anr, source_reference));

          try_stmt.add_catch_clause (new Vala.CatchClause (type_from_string ("SQLHeavy.Error"), "e", catch_block, source_reference));
          block.add_statement (try_stmt);

          get_accessor = new Vala.PropertyAccessor (true, false, false, data_type_get, block, source_reference);
        }

        {
          var block = new Vala.Block (source_reference);
          var try_block = new Vala.Block (source_reference);
          var catch_block = new Vala.Block (source_reference);
          var try_stmt = new Vala.TryStatement (try_block, null, source_reference);

          var call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this", source_reference), @"set_$(data_type_get.to_string ())", source_reference), source_reference);
          call.add_argument (new Vala.StringLiteral (@"\"$(table.field_name (field))\"", source_reference));
          call.add_argument (new Vala.MemberAccess (null, "value", source_reference));
          try_block.add_statement (new Vala.ExpressionStatement (call, source_reference));

          var error_call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("GLib"), "error", source_reference), source_reference);
          error_call.add_argument (new Vala.StringLiteral (@"\"Unable to set `$(name)': %s\"", source_reference));
          error_call.add_argument (new Vala.MemberAccess (new Vala.MemberAccess (null, "e"), "message", source_reference));
          catch_block.add_statement (new Vala.ExpressionStatement (error_call, source_reference));

          try_stmt.add_catch_clause (new Vala.CatchClause (type_from_string ("SQLHeavy.Error"), "e", catch_block, source_reference));
          block.add_statement (try_stmt);

          set_accessor = new Vala.PropertyAccessor (false, true, false, data_type, block, source_reference);
        }

        var prop = new Vala.Property (name, data_type, get_accessor, set_accessor, source_reference);
        prop.access = Vala.SymbolAccessibility.PUBLIC;
        cl.add_property (prop);
      }

      switch_section.add_statement (new Vala.ExpressionStatement (emit_changed_notify, source_reference));
      switch_section.add_statement (new Vala.BreakStatement (source_reference));
    }

    private void parse_table (SQLHeavy.Table table, Vala.Namespace ns, Vala.SourceReference source_reference) throws GeneratorError, SQLHeavy.Error {
      var db = table.queryable.database;
      var db_symbol = GLib.Path.get_basename (db.filename).split (".", 2)[0];
      var symbol = @"@$(GLib.Path.get_basename (db_symbol))/$(table.name)";
      var symbol_name = this.get_symbol_name (symbol);

      if ( this.symbol_is_hidden (symbol) )
        return;

      var cl = ns.scope.lookup (symbol_name) as Vala.Class;

      if ( cl == null ) {
        cl = new Vala.Class (symbol_name, source_reference);
        cl.access = Vala.SymbolAccessibility.PUBLIC;
        ns.add_class (cl);
      }

      cl.add_base_type (type_from_string ("SQLHeavy.Row"));

      Vala.SwitchStatement signals_switch;
      {
        var register_notify = new Vala.Method ("emit_change_notification", new Vala.VoidType ());
        register_notify.access = Vala.SymbolAccessibility.PRIVATE;
        register_notify.add_parameter (new Vala.Parameter ("field", type_from_string ("int")));

        var block = new Vala.Block (source_reference);
        var try_block = new Vala.Block (source_reference);
        var catch_block = new Vala.Block (source_reference);
        var try_stmt = new Vala.TryStatement (try_block, null, source_reference);

        var field_name = new Vala.LocalVariable (type_from_string ("string"), "field_name", new Vala.StringLiteral ("null"), source_reference);
        block.add_statement (new Vala.DeclarationStatement (field_name, source_reference));

        block.add_statement (try_stmt);
        var get_field_name = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("this"), "field_name"));
        get_field_name.add_argument (new Vala.MemberAccess (null, "field"));
        try_block.add_statement (new Vala.ExpressionStatement (new Vala.Assignment (new Vala.StringLiteral ("field_name"), get_field_name)));

        var warn_call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.StringLiteral ("GLib"), "warning"), source_reference);
        warn_call.add_argument (new Vala.StringLiteral ("\"" + "Unknown field: %d" + "\"", source_reference));
        warn_call.add_argument (new Vala.MemberAccess (null, "field", source_reference));
        catch_block.add_statement (new Vala.ExpressionStatement (warn_call, source_reference));
        catch_block.add_statement (new Vala.ReturnStatement (null, source_reference));
        try_stmt.add_catch_clause (new Vala.CatchClause (type_from_string ("SQLHeavy.Error"), "e", catch_block, source_reference));

        signals_switch = new Vala.SwitchStatement (new Vala.StringLiteral ("field_name"), source_reference);
        block.add_statement (signals_switch);
        register_notify.body = block;

        cl.add_method (register_notify);
      }

      var con = new Vala.Constructor (source_reference);
      con.body = new Vala.Block (source_reference);

      var conn_call = new Vala.MethodCall (new Vala.MemberAccess (new Vala.MemberAccess (new Vala.StringLiteral ("this"), "field_changed", source_reference), "connect", source_reference), source_reference);
      conn_call.add_argument (new Vala.MemberAccess (new Vala.StringLiteral ("this"), "emit_change_notification", source_reference));

      con.body.add_statement (new Vala.ExpressionStatement (conn_call, source_reference));
      cl.constructor = con;

      for ( var field = 0 ; field < table.field_count ; field++ ) {
        this.parse_field (table, field, cl, signals_switch, source_reference);
      }
    }

    private void parse_database (SQLHeavy.Database db) throws GeneratorError {
      var symbol = "@".concat (GLib.Path.get_basename (db.filename).split (".", 2)[0]);
      var symbol_name = this.get_symbol_name (symbol);
      Vala.Namespace? ns = this.context.root.scope.lookup (symbol_name) as Vala.Namespace;

      Vala.SourceFile source_file = new Vala.SourceFile (this.context, Vala.SourceFileType.NONE, db.filename);
      Vala.SourceReference source_reference = new Vala.SourceReference (source_file);

      if ( ns == null ) {
        ns = new Vala.Namespace (symbol_name, source_reference);
        this.context.root.add_namespace (ns);
      }

      if ( this.symbol_is_hidden (symbol) )
        return;

      try {
        var tables = db.get_tables ();
        foreach ( unowned SQLHeavy.Table table in tables.get_values () ) {
          this.parse_table (table, ns, source_reference);
        }
      } catch ( SQLHeavy.Error e ) {
        throw new GeneratorError.DATABASE ("Database error: %s", e.message);
      }
    }

    public void run () throws GeneratorError {
      if ( output_location == null ) {
        GLib.stderr.printf ("You must supply an output location\n");
        return;
      }

      var parser = new Vala.Parser ();
      parser.parse (this.context);

      foreach ( unowned string dbfile in this.databases ) {
        SQLHeavy.Database db;
        try {
          db = new SQLHeavy.Database (dbfile, SQLHeavy.FileMode.READ);
        } catch ( SQLHeavy.Error e ) {
          throw new GeneratorError.CONFIGURATION ("Unable to open database: %s", e.message);
        }
        this.parse_database (db);
      }

      var resolver = new Vala.SymbolResolver ();
      resolver.resolve (context);

      if (context.report.get_errors () > 0)
        throw new GeneratorError.SYMBOL_RESOLVER ("Error resolving symbols.");

      context.analyzer.analyze (context);

      var code_writer = new Vala.CodeWriter (Vala.CodeWriterType.DUMP);
      code_writer.write_file (this.context, output_location);
    }

    private static string parse_selector (string selector, out bool wildcard) throws GeneratorError {
      wildcard = false;
      string[] real_selector = new string[3];
      var segments = selector.split ("/", 3);

      int pos = 0;
      for ( int seg = 0 ; seg < segments.length ; seg++ ) {
        var first_char = segments[seg].get_char ();

        if ( first_char == '%' || first_char == '@' ) {
          int dest_pos;
          if ( first_char == '%' ) {
            segments[seg] = segments[seg].offset (1);
            dest_pos = 1;
          }
          else
            dest_pos = 0;

          while ( pos < dest_pos ) {
            wildcard = true;
            real_selector[pos] = "*";
            pos++;
          }
        } else if ( pos == 0 && first_char != '*' ) {
          wildcard = true;
          real_selector[0] = "*";
          real_selector[1] = "*";
          pos = 2;
        }

        if ( segments[seg] == "*" )
          wildcard = true;

        if ( pos > 2 || real_selector[pos] != null )
          throw new GeneratorError.SELECTOR ("Invalid selector (%s).", selector);
        real_selector[pos] = segments[seg];
        pos++;
      }

      return string.joinv ("/", real_selector);
    }

    private void parse_metadata () throws GeneratorError, GLib.KeyFileError {
      var metadata = new GLib.KeyFile ();
      metadata.load_from_file (metadata_location, GLib.KeyFileFlags.NONE);

      foreach ( unowned string group in metadata.get_groups () ) {
        bool is_wildcard;
        var selector = parse_selector (group, out is_wildcard);

        var cache = is_wildcard ? this.wildcard_cache : this.cache;
        var properties = cache.get (selector);
        if ( properties == null ) {
          properties = new Vala.HashMap<string, string> (GLib.str_hash, GLib.str_equal, GLib.str_equal);
          cache.set (selector, properties);
        }

        foreach ( unowned string key in metadata.get_keys (group) )
          properties.set (key, metadata.get_string (group, key));
      }
    }

    public void configure () throws GeneratorError {
      if ( metadata_location != null ) {
        try {
          this.parse_metadata ();
        } catch ( GLib.KeyFileError e ) {
          throw new GeneratorError.CONFIGURATION ("Unable to load metadata file: %s", e.message);
        } catch ( GLib.FileError e ) {
          throw new GeneratorError.CONFIGURATION ("Unable to load metadata file: %s", e.message);
        }
      }

      this.context.profile = Vala.Profile.GOBJECT;
      Vala.CodeContext.push (this.context);

      // Default packages
      this.context.add_external_package ("glib-2.0");
      this.context.add_external_package ("gobject-2.0");
      this.context.add_external_package ("sqlheavy-%s".printf (SQLHeavy.Version.api ()));

      foreach ( unowned string pkg in packages ) {
        this.context.add_external_package (pkg);
      }

      foreach ( unowned string source in sources ) {
        if ( source.has_suffix (".vala") ) {
          if ( GLib.FileUtils.test (source, GLib.FileTest.EXISTS) )
            this.context.add_source_file (new Vala.SourceFile (this.context, Vala.SourceFileType.NONE, source));
          else
            throw new GeneratorError.CONFIGURATION (@"Source file '$(source)' does not exist.");
        } else {
          this.databases.prepend (source);
        }
      }
    }

    private static int main (string[] args) {
      try {
        var opt_context = new GLib.OptionContext ("- SQLHeavy ORM Generator");
        opt_context.set_help_enabled (true);
        opt_context.add_main_entries (options, null);
        opt_context.set_summary ("This tool will generate a Vala file which provides an object for each\ntable in the specified database(s), each of which extends the\nSQLHeavyRow class.");
        opt_context.set_description ("Copyright 2010 Evan Nemerson.\nReleased under versions 2.1 and 3 of the LGPL.\n\nFor more information, or to report a bug, see\n<http://code.google.com/p/sqlheavy>");

        opt_context.parse (ref args);
      } catch ( GLib.OptionError e ) {
        GLib.stdout.puts (@"$(e.message)\n");
        GLib.stdout.puts (@"Run '$(args[0]) --help' to see a full list of available command line options.\n");
        return 1;
      }

      if ( sources == null ) {
        GLib.stderr.puts ("No databases specified.\n");
        return 1;
      }

      var generator = new Generator ();
      try {
        generator.configure ();
        generator.run ();
      } catch ( GeneratorError e ) {
        GLib.stderr.puts (@"Error: $(e.message)\n");
        GLib.stdout.puts (@"Run '$(args[0]) --help' to see a full list of available command line options.\n");
        return 1;
      }

      return 0;
    }
  }
}

