// Copyright 2014 Cognitect. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide("com.cognitect.transit.handlers");
goog.require("com.cognitect.transit.util");
goog.require("com.cognitect.transit.types");
goog.require("goog.math.Long");

goog.scope(function () {

    var handlers = com.cognitect.transit.handlers,
        util     = com.cognitect.transit.util,
        types    = com.cognitect.transit.types,
        Long     = goog.math.Long;

    handlers.ctorGuid = 0;

    /**
     * @const
     * @type {string}
     */
    handlers.ctorGuidProperty = "transit$guid$" + util.randomUUID();

    handlers.typeTag = function (ctor) {
        if (ctor == null) {
            return "null";
        } else if (ctor === String) {
            return "string";
        } else if (ctor === Boolean) {
            return "boolean";
        } else if (ctor === Number) {
            return "number";
        } else if (ctor === Array) {
            return "array";
        } else if (ctor === Object) {
            return "map";
        } else {
            var tag = ctor[handlers.ctorGuidProperty];
            if (tag == null) {
                if (typeof Object.defineProperty != "undefined") {
                    tag = ++handlers.ctorGuid;
                    Object.defineProperty(ctor, handlers.ctorGuidProperty, {
                        value: tag,
                        enumerable: false
                    });
                } else {
                    ctor[handlers.ctorGuidProperty] = tag = ++handlers.ctorGuid;
                }
            }
            return tag;
        }
    };

    handlers.constructor = function (x) {
        if (x == null) {
            return null;
        } else {
            return x.constructor;
        }
    };

    handlers.padZeros = function (n, m) {
        var s = n.toString();
        for (var i = s.length; i < m; i++) {
            s = "0" + s;
        }
        return s;
    };

    handlers.stringableKeys = function (m) {
        var stringable = false,
            ks = util.objectKeys(m);

        for (var i = 0; i < ks.length; i++) {
        }

        return true;
    };

    /**
     * @constructor
     */
    handlers.NilHandler = function Transit$NilHandler() {
    };
    handlers.NilHandler.prototype.tag = function (v) {
        return "_";
    };
    handlers.NilHandler.prototype.rep = function (v) {
        return null;
    };
    handlers.NilHandler.prototype.stringRep = function (v) {
        return "null";
    };

    /**
     * @constructor
     */
    handlers.StringHandler = function Transit$StringHandler() {
    };
    handlers.StringHandler.prototype.tag = function (v) {
        return "s";
    };
    handlers.StringHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.StringHandler.prototype.stringRep = function (v) {
        return v;
    };

    /**
     * @constructor
     */
    handlers.NumberHandler = function Transit$NumberHandler() {
    };
    handlers.NumberHandler.prototype.tag = function (v) {
        return "i";
    };
    handlers.NumberHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.NumberHandler.prototype.stringRep = function (v) {
        return v.toString();
    };

    /**
     * @constructor
     */
    handlers.IntegerHandler = function Transit$IntegerHandler() {
    };
    handlers.IntegerHandler.prototype.tag = function (v) {
        return "i";
    };
    handlers.IntegerHandler.prototype.rep = function (v) {
        return v.toString();
    };
    handlers.IntegerHandler.prototype.stringRep = function (v) {
        return v.toString();
    };

    /**
     * @constructor
     */
    handlers.BooleanHandler = function Transit$BooleanHandler() {
    };
    handlers.BooleanHandler.prototype.tag = function (v) {
        return "?";
    };
    handlers.BooleanHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.BooleanHandler.prototype.stringRep = function (v) {
        return v.toString();
    };

    /**
     * @constructor
     */
    handlers.ArrayHandler = function Transit$ArrayHandler() {
    };
    handlers.ArrayHandler.prototype.tag = function (v) {
        return "array";
    };
    handlers.ArrayHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.ArrayHandler.prototype.stringRep = function (v) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.MapHandler = function Transit$MapHandler() {
    };
    handlers.MapHandler.prototype.tag = function (v) {
        return "map";
    };
    handlers.MapHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.MapHandler.prototype.stringRep = function (v) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.VerboseDateHandler = function Transit$VerboseDateHandler() {
    };
    handlers.VerboseDateHandler.prototype.tag = function (v) {
        return "t";
    };
    handlers.VerboseDateHandler.prototype.rep = function (v) {
        return v.getUTCFullYear() + "-" + handlers.padZeros(v.getUTCMonth() + 1, 2) + "-" +
            handlers.padZeros(v.getUTCDate(), 2) + "T" + handlers.padZeros(v.getUTCHours(), 2) + ":" +
            handlers.padZeros(v.getUTCMinutes(), 2) + ":" + handlers.padZeros(v.getUTCSeconds(), 2) + "." +
            handlers.padZeros(v.getUTCMilliseconds(), 3) + "Z";
    };
    handlers.VerboseDateHandler.prototype.stringRep = function (v, h) {
        return h.rep(v);
    };

    /**
     * @constructor
     */
    handlers.DateHandler = function Transit$DateHandler() {
    };
    handlers.DateHandler.prototype.tag = function (v) {
        return "m";
    };
    handlers.DateHandler.prototype.rep = function (v) {
        return v.valueOf();
    };
    handlers.DateHandler.prototype.stringRep = function (v) {
        return v.valueOf().toString();
    };
    handlers.DateHandler.prototype.getVerboseHandler = function (v) {
        return new handlers.VerboseDateHandler();
    };

    /**
     * @constructor
     */
    handlers.UUIDHandler = function Transit$UUIDHandler() {
    };
    handlers.UUIDHandler.prototype.tag = function (v) {
        return "u";
    };
    handlers.UUIDHandler.prototype.rep = function (v) {
        return v.toString();
    };
    handlers.UUIDHandler.prototype.stringRep = function (v) {
        return v.toString();
    };

    /**
     * @constructor
     */
    handlers.KeywordHandler = function Transit$KeywordHandler() {
    };
    handlers.KeywordHandler.prototype.tag = function (v) {
        return ":";
    };
    handlers.KeywordHandler.prototype.rep = function (v) {
        return v._name;
    }; // NOTE: should be fqn
    handlers.KeywordHandler.prototype.stringRep = function (v, h) {
        return h.rep(v);
    };

    /**
     * @constructor
     */
    handlers.SymbolHandler = function Transit$SymbolHandler() {
    };
    handlers.SymbolHandler.prototype.tag = function (v) {
        return "$";
    };
    handlers.SymbolHandler.prototype.rep = function (v) {
        return v._name;
    }; // NOTE: should be str
    handlers.SymbolHandler.prototype.stringRep = function (v, h) {
        return h.rep(v);
    };

    /**
     * @constructor
     */
    handlers.TaggedHandler = function Transit$TaggedHandler() {
    };
    handlers.TaggedHandler.prototype.tag = function (v) {
        return v.tag;
    };
    handlers.TaggedHandler.prototype.rep = function (v) {
        return v.rep;
    };
    handlers.TaggedHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.TransitSetHandler = function Transit$TransitSetHandler() {
    };
    handlers.TransitSetHandler.prototype.tag = function (v) {
        return "set";
    };
    handlers.TransitSetHandler.prototype.rep = function (v) {
        var arr = [];
        v.forEach(function (key, set) {
            arr.push(key);
        });
        return types.taggedValue("array", arr);
    };
    handlers.TransitSetHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.TransitArrayMapHandler = function Transit$ArrayMapHandler() {
    };
    handlers.TransitArrayMapHandler.prototype.tag = function (v) {
        return "map";
    };
    handlers.TransitArrayMapHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.TransitArrayMapHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.TransitMapHandler = function Transit$MapHandler() {
    };
    handlers.TransitMapHandler.prototype.tag = function (v) {
        return "map";
    };
    handlers.TransitMapHandler.prototype.rep = function (v) {
        return v;
    };
    handlers.TransitMapHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.BufferHandler = function Transit$BufferHandler() {
    };
    handlers.BufferHandler.prototype.tag = function (v) {
        return "b";
    };
    handlers.BufferHandler.prototype.rep = function (v) {
        return v.toString("base64");
    };
    handlers.BufferHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    /**
     * @constructor
     */
    handlers.Uint8ArrayHandler = function Transit$Uint8ArrayHandler() {
    };
    handlers.Uint8ArrayHandler.prototype.tag = function (v) {
        return "b";
    };
    handlers.Uint8ArrayHandler.prototype.rep = function (v) {
        return util.Uint8ToBase64(v);
    };
    handlers.Uint8ArrayHandler.prototype.stringRep = function (v, h) {
        return null;
    };

    handlers.defaultHandlers = function (hs) {
        hs.set(null, new handlers.NilHandler());
        hs.set(String, new handlers.StringHandler());
        hs.set(Number, new handlers.NumberHandler());
        hs.set(Long, new handlers.IntegerHandler());
        hs.set(Boolean, new handlers.BooleanHandler());
        hs.set(Array, new handlers.ArrayHandler());
        hs.set(Object, new handlers.MapHandler());
        hs.set(Date, new handlers.DateHandler());
        hs.set(types.UUID, new handlers.UUIDHandler());
        hs.set(types.Keyword, new handlers.KeywordHandler());
        hs.set(types.Symbol, new handlers.SymbolHandler());
        hs.set(types.TaggedValue, new handlers.TaggedHandler());
        hs.set(types.TransitSet, new handlers.TransitSetHandler());
        hs.set(types.TransitArrayMap, new handlers.TransitArrayMapHandler());
        hs.set(types.TransitMap, new handlers.TransitMapHandler());

        if (typeof Buffer != "undefined") {
            hs.set(Buffer, new handlers.BufferHandler());
        }

        if (typeof Uint8Array != "undefined") {
            hs.set(Uint8Array, new handlers.Uint8ArrayHandler());
        }

        return hs;
    };

    /**
     * @constructor
     */
    handlers.Handlers = function Transit$Handlers() {
        this.handlers = {};
        handlers.defaultHandlers(this);
    };

    handlers.Handlers.prototype.get = function (ctor) {
        var h = null;
        if (typeof ctor === "string") {
            h = this.handlers[ctor];
        } else {
            h = this.handlers[handlers.typeTag(ctor)];
        }
        if (h != null) {
            return h;
        } else {
            return this.handlers["default"];
        }
    };
    handlers.Handlers.prototype["get"] = handlers.Handlers.prototype.get;

    handlers.validTag = function (tag) {
        switch (tag) {
            case "null":
            case "string":
            case "boolean":
            case "number":
            case "array":
            case "map":
                return false;
                break;
        }
        return true;
    };

    handlers.Handlers.prototype.set = function (ctor, handler) {
        if (typeof ctor === "string" && handlers.validTag(ctor)) {
            this.handlers[ctor] = handler;
        } else {
            this.handlers[handlers.typeTag(ctor)] = handler;
        }
    };

});    
