var d3Script = document.createElement('script');
d3Script.src = 'https://d3js.org/d3.v4.js';

var isD3Loaded = false;
var d3CallbacksStack = [];

d3Script.addEventListener('load', function () {
  isD3Loaded = true;

  d3CallbacksStack.forEach(function (callback) {
    callback();
  })
});

var onD3Load = function (callback) {
  if (isD3Loaded) {
    callback();
  } else {
    d3CallbacksStack.push(callback);
  }
};

document.head.appendChild(d3Script);

exports.Task = extend(TolokaHandlebarsTask, function (options) {
  this._inputs = options.task.input_values;

  TolokaHandlebarsTask.call(this, options);
}, {
    validate: function (solution) {
        var output = solution.output_values;
        var polygons = output.polygons && JSON.parse(output.polygons);
        if ((Array.isArray(polygons) && polygons.length > 0) ||
            output.state !== 'ok'
        ) {
            return this._customValidate(output);
        }

        return this._getError();
    },

    _customValidate: function (solution) {
        var golden = this._inputs.golden_output_values;
        if (!golden) {
            return null;
        }

        var defaultMessage = this._inputs.message_on_unknown_solution;

        if (solution.state !== golden.state) {
            if (golden.state === 'empty' || golden.state === 'not-loaded') {
                return this._getError('На этой фотографии нет знаков,<br>которые необходимо разметить.');
            }

            return this._getError('Oтмечены не все знаки.<br>' + defaultMessage);
        }

        var goldenPolygons = JSON.parse(golden.polygons);
        var originalPolygons = JSON.parse(solution.polygons).filter(function (bbox) {
            return Math.abs(bbox[1][0] - bbox[0][0]) > 10 &&
                Math.abs(bbox[1][1] - bbox[0][1]) > 10;
        });

        if (originalPolygons.length !== goldenPolygons.length) {
            return this._getError(
                originalPolygons.length > goldenPolygons.length ?
                    'Oтмечены объекты, не являющиеся знаками.<br>' + defaultMessage :
                    'Oтмечены не все знаки.<br>' + defaultMessage
            );
        }

        var pairs = [];

        goldenPolygons.forEach(function (goldenBbox) {
            var currentDistance = Infinity;
            var currentBbox = originalPolygons[0];

            var distances = originalPolygons.forEach(function (testBbox) {
                var testDistance = bboxDistance(goldenBbox, testBbox);
                if (testDistance < currentDistance) {
                    currentDistance = testDistance;
                    currentBbox = testBbox;
                }
            });

            pairs.push({
                original: currentBbox,
                golden: goldenBbox
            });

            originalPolygons.splice(originalPolygons.indexOf(currentBbox), 1);
        });

        var insufficientAccuracy = pairs.some(function (pair) {
            var width = Math.abs(pair.golden[1][0] - pair.golden[0][0]);
            var height = Math.abs(pair.golden[1][1] - pair.golden[0][1]);

            return Math.abs(pair.golden[0][0] - pair.original[0][0]) > 0.3 * width ||
                Math.abs(pair.golden[1][0] - pair.original[1][0]) > 0.3 * width ||
                Math.abs(pair.golden[0][1] - pair.original[0][1]) > 0.3 * height ||
                Math.abs(pair.golden[1][1] - pair.original[1][1]) > 0.3 * height;
        });

        if (insufficientAccuracy) {
            return this._getError(
                'Знаки выделены неточно.<br>' +
                'Знаки нужно выделять минимальной рамкой, в которую знак помещается целиком.<br>' +
                defaultMessage
            );
        }

        this.setSolution({
            task_id: this.getTask().id,
            output_values: golden
        });
    },

    _getError: function (meesage) {
        return {
            task_id: this.getTask().id,
            errors: {
                __TASK__: {
                    code: 'NOT_ALLOWED',
                    message: meesage || 'На каждой фотографии<br>необходимо разметить знаки<br>или указать, что знаков на ней нет!'
                }
            }
        };
    },

    onValidationFail: function (data) {
        this._popup
            .find('.task-popup__message')
            .html(data.errors.__TASK__.message);

        this._popup.addClass('shown');
    },

    onRender: function() {
        var context = this;

        this._popup = $(this.getDOMElement()).find('.task-popup');

        this._popup.find('.task-popup__close').on('click', function (e) {
            e.stopPropagation();
            context._popup.removeClass('shown');
        });

        var element = this.getDOMElement();
        var nextButton = $(element).find('.next-photo');
        nextButton.on('click', function () {
            var frameSize = window.innerHeight - 10;
            var currentScroll = element.parentElement.scrollTop;

            var index = Math.round(currentScroll / frameSize);
            element.parentElement.scrollTop = (index + 1) * frameSize;
        });

        var image = new Image();

        onD3Load(function () {
            image.addEventListener('load', function () {
                init.bind(context)(image);
            });
            image.addEventListener('error', function () {
                $('input[name=\"state\"][value=\"not-loaded\"]').click();

                context.setSolution({
                    task_id: context.getTask().id,
                    output_values: {polygons: '[]'}
                });
            });

            image.src = context.getTask().input_values.source;
        });
    }
});

function init(image) {
    var POINT_RADIUS = 3;

    var canvas = d3.select(this.getDOMElement()).select('canvas');
    var context = canvas.node().getContext('2d');

    var canvasWidth = canvas.node().width = window.innerWidth - 360;
    var canvasHeight = canvas.node().height = window.innerHeight - 60;

    var activeObjects = {
        point: null,
        rectangle: null
    };

    var imageRect = {
        scale: Math.min(canvasWidth / image.width, canvasHeight / image.height)
    };

    imageRect.width = imageRect.scale * image.width;
    imageRect.height = imageRect.scale * image.height;

    imageRect.offset = {
        x: (canvasWidth - imageRect.width) / 2,
        y: (canvasHeight - imageRect.height) / 2
    };

    var transform = (function () {
        var transform = {
            x: 0,
            y: 0,
            k: 1
        };

        return {
            get: function () {
                transform = d3.event && d3.event.transform || transform;
                return transform;
            },

            transformPoint: function (point) {
                return [
                    (point[0] - transform.x) / transform.k,
                    (point[1] - transform.y) / transform.k
                ];
            }
        };
    })();

    var setSolution = (function (polygons) {
        this.setSolution({
            task_id: this.getTask().id,
            output_values: {polygons: JSON.stringify(polygons)}
        });
    }).bind(this);

    var rectangles = {
        _items: [],

        getAll: function () {
            return this._items;
        },

        add: function (points) {
            if (points[0][0] < points[1][0]) {
                if (points[0][1] < points[1][1]) {
                    this._items.push(points);
                } else {
                    this._items.push([[points[0][0], points[1][1]], [points[1][0], points[0][1]]]);
                }
            } else {
                if (points[0][1] < points[1][1]) {
                    this._items.push([[points[1][0], points[0][1]], [points[0][0], points[1][1]]]);
                } else {
                    this._items.push([points[1], points[0]]);
                }
            }

            this._export();
        },

        remove: function (rectangle) {
            var index = this._items.indexOf(rectangle);
            this._items.splice(index, 1);

            this._export();
        },

        removeAll: function () {
            this._items = [];

            this._export();
        },

        findByPoint: function (point) {
            var radius = 2 * POINT_RADIUS / transform.get().k;
            for (var i = this._items.length - 1; i >= 0; i--) {
                var rectangle = this._items[i];
                if (rectangle[0][0] - radius < point[0] && rectangle[0][1] - radius < point[1] &&
                    rectangle[1][0] + radius > point[0] && rectangle[1][1] + radius > point[1]
                ) {
                    return rectangle;
                }
            }
            return null;
        },

        _export: function () {
            var items = this._items.map(function (item) {
                return [
                    [
                        (item[0][0] - imageRect.offset.x) / imageRect.scale,
                        (item[0][1] - imageRect.offset.y) / imageRect.scale
                    ].map(Math.ceil),
                    [
                        (item[1][0] - imageRect.offset.x) / imageRect.scale,
                        (item[1][1] - imageRect.offset.y) / imageRect.scale
                    ].map(Math.floor)
                ];
            });

            setSolution(items);
        }
    };

    var zoom = d3.zoom();
    canvas.call(zoom);

    zoom.translateExtent([
        [0, 0],
        [canvasWidth, canvasHeight]
    ]);

    zoom.scaleExtent([1, 10]);

    zoom.on('zoom', render);
    render();

    var node = $(this.getDOMElement());
    var state = 'ok';

    node.find('.state').click(function () {
        state = node.find('input[name=\"state\"]:checked').val();

        if (state !== 'ok') {
            rectangles.removeAll();

            activeObjects = {
                point: null,
                rectangle: null
            };

            render();
        }
    });

    canvas.on('click', function () {
        if (state !== 'ok') {
            return;
        }

        var point = transform.transformPoint(d3.mouse(this));

        if (!checkPointInImage(point)) {
            return;
        }

        if (activeObjects.point) {
            if (!checkEqualPoints(activeObjects.point, point)) {
                rectangles.add([activeObjects.point, point]);

                activeObjects.point = null;
                activeObjects.rectangle = null;
            } else {
                activeObjects.point = null;
            }
        } else {
            var rectangle = rectangles.findByPoint(point);
            if (rectangle) {
                rectangles.remove(rectangle);

                var points = complementRectangle(rectangle);
                var equalPoints = points.filter(function (testPoint) {
                    return checkEqualPoints(point, testPoint);
                });
                if (equalPoints.length === 1) {
                    activeObjects.point = points[(points.indexOf(equalPoints[0]) + 2) % 4];
                    activeObjects.rectangle = [activeObjects.point, equalPoints[0]];
                }
            } else {
                activeObjects.point = point;
            }
        }

        render();
    });


    canvas.on('mousemove', function () {
        var point = transform.transformPoint(d3.mouse(this));

        var cursor;

        if (checkPointInImage(point)) {
            if (activeObjects.point) {
                activeObjects.rectangle = [activeObjects.point, point];
                cursor = 'add';

                render();
            } else {
                var rectangle = rectangles.findByPoint(point);
                if (!rectangle) {
                    cursor = 'add';
                } else {
                    var equalPoint = complementRectangle(rectangle).find(function (testPoint) {
                        return checkEqualPoints(point, testPoint);
                    });

                    cursor = equalPoint ? 'edit' : 'remove';
                }
            }
        }

        canvas.node().classList.remove('add-cursor', 'edit-cursor', 'remove-cursor');
        if (cursor) {
            canvas.node().classList.add(cursor + '-cursor');
        }
    });

    function checkPointInImage(point) {
        return point[0] > imageRect.offset.x &&
            point[0] < imageRect.offset.x + imageRect.width &&
            point[1] > imageRect.offset.y &&
            point[1] < imageRect.offset.y + imageRect.height;
    }

    function checkEqualPoints(point1, point2) {
        var radius = 2 * POINT_RADIUS / transform.get().k;
        return point1[0] - radius < point2[0] && point1[0] + radius > point2[0] &&
            point1[1] - radius < point2[1] && point1[1] + radius > point2[1];
    }

    function complementRectangle(rectangle) {
        return [
            rectangle[0],
            [rectangle[1][0], rectangle[0][1]],
            rectangle[1],
            [rectangle[0][0], rectangle[1][1]]
        ];
    }

    function renderRectagle(context, rectangle, k) {
        context.beginPath();

        context.lineWidth = 1 / k;

        context.rect(
            rectangle[0][0],
            rectangle[0][1],
            rectangle[1][0] - rectangle[0][0],
            rectangle[1][1] - rectangle[0][1]
        );

        context.fillStyle = 'rgba(255, 255, 255, 0.5)';
        context.fill();

        context.strokeStyle = 'rgb(0, 0, 255)';
        context.stroke();

        context.closePath();

        for (var i = 0; i < 4; i++) {
            var position = ('0' + i.toString(2)).split('').splice(-2);
            renderPoint(context, [rectangle[position[0]][0], rectangle[position[1]][1]], k);
        }
    }

    function renderPoint(context, point, k) {
        context.beginPath();
        context.arc(point[0], point[1], POINT_RADIUS / k, 0, 2 * Math.PI, true);
        context.fillStyle = 'rgb(255, 255, 0)';
        context.fill();
    }

    function render() {
        context.save();

        context.clearRect(0, 0, canvasWidth, canvasHeight);

        var params = transform.get();
        if (params) {
            context.translate(params.x, params.y);
            context.scale(params.k, params.k);
        }

        context.drawImage(
            image,
            imageRect.offset.x,
            imageRect.offset.y,
            imageRect.width,
            imageRect.height
        );

        rectangles.getAll().forEach(function (rectangle) {
            renderRectagle(context, rectangle, params.k);
        });

        if (activeObjects.rectangle) {
            renderRectagle(context, activeObjects.rectangle, params.k);
        } else if (activeObjects.point) {
            renderPoint(context, activeObjects.point, params.k);
        }

        context.restore();
    }
}

function bboxDistance(bbox1, bbox2) {
    return Math.max(pointDistance(bbox1[0], bbox2[0]), pointDistance(bbox1[1], bbox2[1]));
}

function pointDistance(point1, point2) {
    return Math.sqrt(Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2));
}

function extend(ParentClass, constructorFunction, prototypeHash) {
    constructorFunction = constructorFunction || function () {};
    prototypeHash = prototypeHash || {};
    if (ParentClass) {
        constructorFunction.prototype = Object.create(ParentClass.prototype);
    }
    for (var i in prototypeHash) {
        constructorFunction.prototype[i] = prototypeHash[i];
    }
    return constructorFunction;
}
