L.TileLayer.TileJSON = L.TileLayer.Canvas.extend({
    options: {
        debug: false
    },

    tileSize: 256,

    initialize: function (options) {
        L.Util.setOptions(this, options);

        this.drawTile = function (canvas, tilePoint, zoom) {
            var ctx = {
                canvas: canvas,
                tile: tilePoint,
                zoom: zoom
            };

            if (this.options.debug) {
                this._drawDebugInfo(ctx);
            }
            this._draw(ctx);
        };
    },

    _drawDebugInfo: function (ctx) {
        var max = this.tileSize;
        var g = ctx.canvas.getContext('2d');
        g.strokeStyle = '#000000';
        g.fillStyle = '#FFFF00';
        g.strokeRect(0, 0, max, max);
        g.font = "12px Arial";
        g.fillRect(0, 0, 5, 5);
        g.fillRect(0, max - 5, 5, 5);
        g.fillRect(max - 5, 0, 5, 5);
        g.fillRect(max - 5, max - 5, 5, 5);
        g.fillRect(max / 2 - 5, max / 2 - 5, 10, 10);
        g.strokeText(ctx.tile.x + ' ' + ctx.tile.y + ' ' + ctx.zoom, max / 2 - 30, max / 2 - 10);
    },

    _tilePoint: function (ctx, coords) {
        // start coords to tile 'space'
        var s = ctx.tile.multiplyBy(this.tileSize);

        // actual coords to tile 'space'
        var p = this._map.project(new L.LatLng(coords[1], coords[0]));

        // point to draw        
        var x = Math.round(p.x - s.x);
        var y = Math.round(p.y - s.y);
        return {
            x: x,
            y: y
        };
    },

    _clip: function (ctx, points) {
        var nw = ctx.tile.multiplyBy(this.tileSize);
        var se = nw.add(new L.Point(this.tileSize, this.tileSize));
        var bounds = new L.Bounds([nw, se]);
        var len = points.length;
        var out = [];

        for (var i = 0; i < len - 1; i++) {
            var seg = L.LineUtil.clipSegment(points[i], points[i + 1], bounds, i);
            if (!seg) {
                continue;
            }
            out.push(seg[0]);
            // if segment goes out of screen, or it's the last one, it's the end of the line part
            if ((seg[1] !== points[i + 1]) || (i === len - 2)) {
                out.push(seg[1]);
            }
        }
        return out;
    },

    _isActuallyVisible: function (coords) {
        var coord = coords[0];
        var min = [coord.x, coord.y], max = [coord.x, coord.y];
        for (var i = 1; i < coords.length; i++) {
            coord = coords[i];
            min[0] = Math.min(min[0], coord.x);
            min[1] = Math.min(min[1], coord.y);
            max[0] = Math.max(max[0], coord.x);
            max[1] = Math.max(max[1], coord.y);
        }
        var diff0 = max[0] - min[0];
        var diff1 = max[1] - min[1];
        if (this.options.debug) {
            console.log(diff0 + ' ' + diff1);
        }
        var visible = diff0 > 1 || diff1 > 1;
        return visible;
    },

    _drawPoint: function (ctx, geom, style) {
        if (!style) {
            return;
        }
        
        var p = this._tilePoint(ctx, geom);
        var c = ctx.canvas;
        var g = c.getContext('2d');
        g.beginPath();
        g.fillStyle = style.color;
        g.arc(p.x, p.y, style.radius, 0, Math.PI * 2);
        g.closePath();
        g.fill();
        g.restore();
    },

    _drawLineString: function (ctx, geom, style) {
        if (!style) {
            return;
        }
        
        var coords = geom, proj = [], i;
        coords = this._clip(ctx, coords);
        coords = L.LineUtil.simplify(coords, 1);
        for (i = 0; i < coords.length; i++) {
            proj.push(this._tilePoint(ctx, coords[i]));
        }
        if (!this._isActuallyVisible(proj)) {
            return;
        }

        var g = ctx.canvas.getContext('2d');
        g.strokeStyle = style.color;
        g.lineWidth = style.size;
        g.beginPath();
        for (i = 0; i < proj.length; i++) {
            var method = (i === 0 ? 'move' : 'line') + 'To';
            g[method](proj[i].x, proj[i].y);
        }
        g.stroke();
        g.restore();
    },

    _drawPolygon: function (ctx, geom, style) {
        if (!style) {
            return;
        }
        
        for (var el = 0; el < geom.length; el++) {
            var coords = geom[el], proj = [], i;
            coords = this._clip(ctx, coords);
            for (i = 0; i < coords.length; i++) {
                proj.push(this._tilePoint(ctx, coords[i]));
            }
            if (!this._isActuallyVisible(proj)) {
                continue;
            }

            var g = ctx.canvas.getContext('2d');
            var outline = style.outline;
            g.fillStyle = style.color;
            if (outline) {
                g.strokeStyle = outline.color;
                g.lineWidth = outline.size;
            }
            g.beginPath();
            for (i = 0; i < proj.length; i++) {
                var method = (i === 0 ? 'move' : 'line') + 'To';
                g[method](proj[i].x, proj[i].y);
            }
            g.closePath();
            g.fill();
            if (outline) {
                g.stroke();
            }
        }
    },

    _draw: function (ctx) {
        // NOTE: this is the only part of the code that depends from external libraries (actually, jQuery only).        
        var loader = $.getJSON;

        var nwPoint = ctx.tile.multiplyBy(this.tileSize);
        var sePoint = nwPoint.add(new L.Point(this.tileSize, this.tileSize));
        var nwCoord = this._map.unproject(nwPoint, ctx.zoom, true);
        var seCoord = this._map.unproject(sePoint, ctx.zoom, true);
        var bounds = [nwCoord.lng, seCoord.lat, seCoord.lng, nwCoord.lat];

        var url = this.createUrl(bounds);
        var self = this, j;
        loader(url, function (data) {
            for (var i = 0; i < data.features.length; i++) {
                var feature = data.features[i];
                var style = self.styleFor(feature);

                var type = feature.geometry.type;
                var geom = feature.geometry.coordinates;
                var len = geom.length;
                switch (type) {
                    case 'Point':
                        self._drawPoint(ctx, geom, style);
                        break;

                    case 'MultiPoint':
                        for (j = 0; j < len; j++) {
                            self._drawPoint(ctx, geom[j], style);
                        }
                        break;

                    case 'LineString':
                        self._drawLineString(ctx, geom, style);
                        break;

                    case 'MultiLineString':
                        for (j = 0; j < len; j++) {
                            self._drawLineString(ctx, geom[j], style);
                        }
                        break;

                    case 'Polygon':
                        self._drawPolygon(ctx, geom, style);
                        break;

                    case 'MultiPolygon':
                        for (j = 0; j < len; j++) {
                            self._drawPolygon(ctx, geom[j], style);
                        }
                        break;

                    default:
                        throw new Error('Unmanaged type: ' + type);
                }
            }
        });
    },

    // NOTE: a placeholder for a function that, given a tile context, returns a string to a GeoJSON service that retrieve features for that context
    createUrl: function (bounds) {
        // override with your code
    },

    // NOTE: a placeholder for a function that, given a feature, returns a style object used to render the feature itself
    styleFor: function (feature) {
        // override with your code
    }
});