define('bitbucket/internal/feature/file-content/lfs-file-handler', [
    'jquery',
    'lodash',
    'bitbucket/util/navbuilder',
    'bitbucket/util/server',
    'bitbucket/internal/bbui/image-differ/image-differ',
    'bitbucket/internal/feature/comments',
    'bitbucket/internal/feature/file-content/diff-view-options',
    'bitbucket/internal/feature/file-content/diff-view-segment-types',
    'bitbucket/internal/feature/file-content/handlers/diff-handler/diff-handler-internal',
    'bitbucket/internal/feature/file-content/request-diff',
    'bitbucket/internal/feature/file-content/request-source',
    'bitbucket/internal/model/file-change',
    'bitbucket/internal/model/file-change-types',
    'bitbucket/internal/model/file-content-modes'
], function(
    $,
    _,
    nav,
    server,
    imageDiffer,
    comments,
    DiffViewOptions,
    DiffSegmentTypes,
    diffHandlerInternal,
    requestDiff,
    requestSource,
    FileChange,
    FileChangeTypes,
    fileContentModes
) {
    'use strict';

    var lfsMatchers = {
        version: /version https:\/\/git-lfs\.github\.com\/spec\/v(\d)/,
        oid: /oid sha256:([a-fA-F0-9]{64})/,
        size: /size ([0-9]*)/
    };

    var MAX_INLINE_IMAGE_SIZE = 10 * 1024 * 1024; //10MB

    var webImageContentTypes = {
        bmp: 'image/bmp',
        gif: 'image/gif',
        ico: 'image/x-icon',
        jpg: 'image/jpeg',
        jpeg: 'image/jpeg',
        png: 'image/png'
    };

    function buildLFSUrl(href, size, filePath) {
        var lfsUrl = nav.parse(href);

        _.forEach(getResponseHeaders(size, filePath), function(value, header){
            lfsUrl.addQueryParam(header, value);
        });

        return lfsUrl.toString();
    }

    function enrichResponseObjects(paths, response){
        return response.objects.map(function(responseObject, i){
            var href = _.get(responseObject, 'actions.download.href');
            var path = paths[i];
            var extension = path.extension.toLowerCase();
            var imageContentType = webImageContentTypes[extension];
            var extraProps = {
                fileName: path.name,
                extension: extension,
                href: href && buildLFSUrl(href, responseObject.size, path),
                inlineImage: !!(responseObject.size <= MAX_INLINE_IMAGE_SIZE  && imageContentType)
            };

            return _.extend({}, responseObject, extraProps);
        });
    }

    function extractLFSMetaDataFromLines(lines) {
        var lfsMetaData = {};
        var lfsFields = Object.keys(lfsMatchers);

        function lineToMetaData(line, index) {
            var currentField = lfsFields[index];
            var matches = line.match(lfsMatchers[currentField]);
            if (matches) {
                return (lfsMetaData[currentField] = matches[1]);
            }
            return false;
        }

        return lines.length === 3 && lines.every(lineToMetaData) && lfsMetaData;
    }

    function extractLFSMetaDataFromSegments(segments) {
        var destinationLines = [];
        var sourceLines = [];

        segments.forEach(function(segment){
            var segmentLines = _.pluck(segment.lines, 'line');

            switch (segment.type) {
                case DiffSegmentTypes.ADDED:
                    sourceLines.push.apply(destinationLines, segmentLines);
                    break;
                case DiffSegmentTypes.REMOVED:
                    destinationLines.push.apply(sourceLines, segmentLines);
                    break;
                default: //CONTEXT
                    sourceLines.push.apply(sourceLines, segmentLines);
                    destinationLines.push.apply(destinationLines, segmentLines);
            }
        });

        return [destinationLines, sourceLines].map(extractLFSMetaDataFromLines);
    }

    function getLFSDownloadData(lfsMetaDataObjects, paths) {
        if (!lfsMetaDataObjects.length) {
            return reject();
        }

        var restConfig = {
            url: nav.currentRepo().clone('git').addPathComponents('info', 'lfs', 'objects', 'batch').build(),
            type: 'POST',
            dataType: 'json',
            accepts: {
                json: 'application/vnd.git-lfs+json; charset=UTF-8'
            },
            contentType: 'application/vnd.git-lfs+json; charset=UTF-8',
            data: {
                'operation': 'download',
                'objects': lfsMetaDataObjects
            },
            statusCode: {
                '*': false
            }
        };

        return server.rest(restConfig)
            .then(
                enrichResponseObjects.bind(null, paths),
                _.ary(reject, 0) //suppress error messages
            )
    }

    function getResponseHeaders(size, filePath) {
        var headers = {};
        var imageContentType = webImageContentTypes[filePath.extension.toLowerCase()];

        if (size <= MAX_INLINE_IMAGE_SIZE  && imageContentType) {
            //Render this image in the browser
            //See https://tools.ietf.org/html/rfc6266
            headers['response-content-disposition'] = encodeURIComponent('inline; filename="' + filePath.name + '"; filename*=utf-8\'\'' + encodeURIComponent(filePath.name));
            headers['response-content-type'] = encodeURIComponent(imageContentType);
        } else {
            headers['response-content-disposition'] = encodeURIComponent('attachment; filename="' + filePath.name + '"; filename*=utf-8\'\'' + encodeURIComponent(filePath.name));
        }

        return headers;
    }

    function handleLFSDiff(options){
        if (options.isExcerpt) {
            return reject();
        }

        var fileChange = new FileChange(options.fileChange);
        var diffOptions = _.extend({}, options, {
            withComments: options.commentMode !== comments.commentMode.NONE
        });

        //Try and predict if this will be displayed as a side-by-side diff if it's not an LFS file
        //and adjust the diff options accordingly to ensure a cache hit if the diff is handled by diff-handler.js
        //Needs to match SBS selection logic in diff-handler.js
        if (DiffViewOptions.get('diffType') === 'side-by-side' && fileChange.getType() !== FileChangeTypes.ADD && fileChange.getType() !== FileChangeTypes.DELETE) {
            diffOptions = _.extend(diffOptions, {
                contextLines: diffHandlerInternal.infiniteContext,
                withComments: false
            });
        }

        return requestDiff(fileChange, diffOptions).then(function(data) {
            var diff = data.diff;
            if (!diff || diff.binary || !isLFSLikeDiff(diff)) {
                return reject();
            }

            var lfsMetaDataObjects = _.compact(extractLFSMetaDataFromSegments(diff.hunks[0].segments));
            var paths = _.compact([diff.destination, diff.source]);

            if (lfsMetaDataObjects.length !== paths.length) {
                return reject();
            }

            return getLFSDownloadData(lfsMetaDataObjects, paths).then(renderDiffView.bind(null, options.$container, !diff.destination));
        }, function () {
            return reject();
        });
    }

    function handleLFSSource(options){
        return requestSource(options.fileChange).then(function(data) {
            if (data.binary || data.start !== 0 || data.size !== 3) {
                return reject();
            }

            var lfsMetaDataObject = extractLFSMetaDataFromLines(_.pluck(data.lines, 'text'));

            if (!lfsMetaDataObject) {
                return reject();
            }

            var path = options.fileChange.path;

            return getLFSDownloadData([lfsMetaDataObject], [path]).then(renderSourceView.bind(null, options.$container, options.$toolbar));
        }, function () {
            return reject();
        });
    }

    function isLFSLikeDiff(diff) {
        //cheap heuristic
        var lfsDiffShape = {
            destinationLine: diff.destination ? 1 : 0, //for deleted files
            destinationSpan: diff.destination ? 3 : 0, //for deleted files
            sourceLine: diff.source ? 1 : 0, //for added files
            sourceSpan: diff.source ? 3 : 0, //for added files
            truncated: false
        };

        return !!(diff.hunks && diff.hunks.length === 1 && _.isMatch(diff.hunks[0], lfsDiffShape));
    }

    function reject(data) {
        return $.Deferred().reject(data);
    }

    function renderSourceView($container, $toolbar, lfsFiles){
        var sourceFile = lfsFiles[0];

        $container.append(bitbucket.internal.feature.fileContent.lfs.source({
            file: sourceFile
        }));

        $container.parent().addClass('lfs-file-content');

        $toolbar.find('.raw-view-link').attr('href', sourceFile.href).toggle(!sourceFile.error);
    }

    function renderDiffView($container, isDelete, lfsFiles){
        $container.append(bitbucket.internal.feature.fileContent.lfs.diff({
            files: {
                'new': !isDelete ? lfsFiles[0] : null,  //`New` is always the first file, except when the file has been deleted
                'old': !isDelete ? lfsFiles[1] : lfsFiles[0]
            }
        }));

        $container.parent().addClass('lfs-file-content');

        if (_.where(lfsFiles, { inlineImage: true }).length === 2) {
            return imageDiffer.init($container.find('.binary-container'));
        }
    }

    return function handleLFSFile(options) {
        switch (options.contentMode) {
            case fileContentModes.SOURCE:
                return handleLFSSource(options);
            case fileContentModes.DIFF:
                return handleLFSDiff(options);
            default:
                return reject();
        }
    }
});


(function() {
    require('bitbucket/feature/files/file-handlers').register({
        weight: 100,
        handle: function() {
            return require('bitbucket/internal/feature/file-content/lfs-file-handler').apply(null, arguments);
        }
    });
})();
