// 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.impl.decoder");
goog.require("com.cognitect.transit.util");
goog.require("com.cognitect.transit.delimiters");
goog.require("com.cognitect.transit.caching");
goog.require("com.cognitect.transit.types");

goog.scope(function () {

    var decoder = com.cognitect.transit.impl.decoder,
        util    = com.cognitect.transit.util,
        d       = com.cognitect.transit.delimiters,
        caching = com.cognitect.transit.caching,
        types   = com.cognitect.transit.types;

    // =========================================================================
    // Decoder

    /**
     * @constructor
     */
    decoder.Tag = function Transit$Tag(s) {
        this.str = s;
    };

    decoder.tag = function (s) {
        return new decoder.Tag(s);
    };

    decoder.isTag = function (x) {
        return x && (x instanceof decoder.Tag);
    };

    decoder.isGroundHandler = function (handler) {
        switch (handler) {
            case "_":
            case "s":
            case "?":
            case "i":
            case "d":
            case "b":
            case "'":
            case "array":
            case "map":
                return true;
        }
        return false;
    };

    /**
     * A transit decoder
     * @constructor
     */
    decoder.Decoder = function Transit$Decoder(options) {
        this.options = options || {};
        this.handlers = {};
        for (var h in this.defaults.handlers) {
            this.handlers[h] = this.defaults.handlers[h];
        }
        for (var h in this.options["handlers"]) {
            if (decoder.isGroundHandler(h)) {
                throw new Error("Cannot override handler for ground type \"" + h + "\"");
            }
            this.handlers[h] = this.options["handlers"][h];
        }
        this.preferStrings = this.options["preferStrings"] != null ? this.options["preferStrings"] : this.defaults.preferStrings;
        this.preferBuffers = this.options["preferBuffers"] != null ? this.options["preferBuffers"] : this.defaults.preferBuffers;
        this.defaultHandler = this.options["defaultHandler"] || this.defaults.defaultHandler;
        /* NOT PUBLIC */
        this.mapBuilder = this.options["mapBuilder"];
        this.arrayBuilder = this.options["arrayBuilder"];
    };


    decoder.Decoder.prototype.defaults = {
        handlers: {
            "_": function (v, d) {
                return types.nullValue();
            },
            "?": function (v, d) {
                return types.boolValue(v);
            },
            "b": function (v, d) {
                return types.binary(v, d);
            },
            "i": function (v, d) {
                return types.intValue(v);
            },
            "n": function (v, d) {
                return types.bigInteger(v);
            },
            "d": function (v, d) {
                return types.floatValue(v);
            },
            "f": function (v, d) {
                return types.bigDecimalValue(v);
            },
            "c": function (v, d) {
                return types.charValue(v);
            },
            ":": function (v, d) {
                return types.keyword(v);
            },
            "$": function (v, d) {
                return types.symbol(v);
            },
            "r": function (v, d) {
                return types.uri(v);
            },
            "z": function (v, d) {
                return types.specialDouble(v);
            },

            // tagged
            "'": function (v, d) {
                return v;
            },
            "m": function (v, d) {
                return types.date(v);
            },
            "t": function (v, d) {
                return types.verboseDate(v);
            },
            "u": function (v, d) {
                return types.uuid(v);
            },
            "set": function (v, d) {
                return types.set(v);
            },
            "list": function (v, d) {
                return types.list(v);
            },
            "link": function (v, d) {
                return types.link(v);
            },
            "cmap": function (v, d) {
                return types.map(v, false);
            }
        },
        defaultHandler: function (c, val) {
            return types.taggedValue(c, val);
        },
        preferStrings: true,
        preferBuffers: true
    };

    /**
     * @param {*} node
     * @param {*} cache
     * @param {boolean=} asMapKey
     * @param {boolean=} tagValue
     * @returns {*}
     */
    decoder.Decoder.prototype.decode = function (node, cache, asMapKey, tagValue) {
        if (node == null) return null;

        var t = typeof node;

        switch (t) {
            case "string":
                return this.decodeString(node, cache, asMapKey, tagValue);
                break;
            case "object":
                if (util.isArray(node)) {
                    if (node[0] === "^ ") {
                        return this.decodeArrayHash(node, cache, asMapKey, tagValue);
                    } else {
                        return this.decodeArray(node, cache, asMapKey, tagValue);
                    }
                } else {
                    return this.decodeHash(node, cache, asMapKey, tagValue);
                }
                break;
        }

        return node;
    };
    decoder.Decoder.prototype["decode"] = decoder.Decoder.prototype.decode;

    decoder.Decoder.prototype.decodeString = function (string, cache, asMapKey, tagValue) {
        if (caching.isCacheable(string, asMapKey)) {
            var val = this.parseString(string, cache, false);
            if (cache) {
                cache.write(val, asMapKey);
            }
            return val;
        } else if (caching.isCacheCode(string)) {
            return cache.read(string, asMapKey);
        } else {
            return this.parseString(string, cache, asMapKey);
        }
    };

    decoder.Decoder.prototype.decodeHash = function (hash, cache, asMapKey, tagValue) {
        var ks = util.objectKeys(hash),
            key = ks[0],
            tag = ks.length == 1 ? this.decode(key, cache, false, false) : null;

        if (decoder.isTag(tag)) {
            var val = hash[key],
                handler = this.handlers[tag.str];
            if (handler != null) {
                return handler(this.decode(val, cache, false, true), this);
            } else {
                return types.taggedValue(tag.str, this.decode(val, cache, false, false));
            }
        } else if (this.mapBuilder) {
            if ((ks.length < (types.SMALL_ARRAY_MAP_THRESHOLD * 2)) && this.mapBuilder.fromArray) {
                var nodep = [];
                for (var i = 0; i < ks.length; i++) {
                    var strKey = ks[i];
                    nodep.push(this.decode(strKey, cache, true, false));
                    nodep.push(this.decode(hash[strKey], cache, false, false));
                }
                return this.mapBuilder.fromArray(nodep, hash);
            } else {
                var ret = this.mapBuilder.init(hash);
                for (var i = 0; i < ks.length; i++) {
                    var strKey = ks[i];
                    ret = this.mapBuilder.add(ret,
                        this.decode(strKey, cache, true, false),
                        this.decode(hash[strKey], cache, false, false),
                        hash);
                }
                return this.mapBuilder.finalize(ret, hash);
            }
        } else {
            var nodep = [];

            for (var i = 0; i < ks.length; i++) {
                var strKey = ks[i];
                nodep.push(this.decode(strKey, cache, true, false));
                nodep.push(this.decode(hash[strKey], cache, false, false));
            }

            return types.map(nodep, false);
        }
    };

    decoder.Decoder.prototype.decodeArrayHash = function (node, cache, asMapKey, tagValue) {
        if (this.mapBuilder) {
            if ((node.length < ((types.SMALL_ARRAY_MAP_THRESHOLD * 2) + 1)) && this.mapBuilder.fromArray) {
                var nodep = [];
                for (var i = 1; i < node.length; i += 2) {
                    nodep.push(this.decode(node[i], cache, true, false));
                    nodep.push(this.decode(node[i + 1], cache, false, false));
                }
                return this.mapBuilder.fromArray(nodep, node);
            } else {
                var ret = this.mapBuilder.init(node);
                for (var i = 1; i < node.length; i += 2) {
                    ret = this.mapBuilder.add(ret,
                        this.decode(node[i], cache, true, false),
                        this.decode(node[i + 1], cache, false, false),
                        node)
                }
                return this.mapBuilder.finalize(ret, node);
            }
        } else {
            var nodep = [];

            // collect keys
            for (var i = 1; i < node.length; i += 2) {
                nodep.push(this.decode(node[i], cache, true, false));
                nodep.push(this.decode(node[i + 1], cache, false, false));
            }

            return types.map(nodep, false);
        }
    };

    decoder.Decoder.prototype.decodeArray = function (node, cache, asMapKey, tagValue) {
        if (tagValue) {
            var ret = [];
            for (var i = 0; i < node.length; i++) {
                ret.push(this.decode(node[i], cache, asMapKey, false));
            }
            return ret;
        } else {
            var cacheIdx = cache && cache.idx;
            // tagged value as 2-array case
            if ((node.length === 2) &&
                (typeof node[0] === "string")) {
                var tag = this.decode(node[0], cache, false, false);
                if (decoder.isTag(tag)) {
                    var val = node[1],
                        handler = this.handlers[tag.str];
                    if (handler != null) {
                        var ret = handler(this.decode(val, cache, asMapKey, true), this);
                        return ret;
                    } else {
                        return types.taggedValue(tag.str, this.decode(val, cache, asMapKey, false))
                    }
                }
            }

            // rewind cache
            if (cache && (cacheIdx != cache.idx)) {
                cache.idx = cacheIdx;
            }

            if (this.arrayBuilder) {
                // NOTE: hard coded for ClojureScript for now - David
                if (node.length <= 32 && this.arrayBuilder.fromArray) {
                    var arr = [];
                    for (var i = 0; i < node.length; i++) {
                        arr.push(this.decode(node[i], cache, asMapKey, false));
                    }
                    return this.arrayBuilder.fromArray(arr, node);
                } else {
                    var ret = this.arrayBuilder.init(node);
                    for (var i = 0; i < node.length; i++) {
                        ret = this.arrayBuilder.add(ret, this.decode(node[i], cache, asMapKey, false), node);
                    }
                    return this.arrayBuilder.finalize(ret, node);
                }
            } else {
                var ret = [];
                for (var i = 0; i < node.length; i++) {
                    ret.push(this.decode(node[i], cache, asMapKey, false));
                }
                return ret;
            }
        }
    };

    decoder.Decoder.prototype.parseString = function (string, cache, asMapKey) {
        if (string.charAt(0) === d.ESC) {
            var c = string.charAt(1);
            if (c === d.ESC || c === d.SUB || c === d.RES) {
                return string.substring(1);
            } else if (c === d.TAG) {
                return decoder.tag(string.substring(2));
            } else {
                var handler = this.handlers[c];
                if (handler == null) {
                    return this.defaultHandler(c, string.substring(2));
                } else {
                    return handler(string.substring(2), this);
                }
            }
        } else {
            return string;
        }
    };

    decoder.decoder = function (options) {
        return new decoder.Decoder(options);
    };

});
