'use strict';

var JpipCodestreamClient = function JpipCodestreamClientClosure(options) {
    options = options || {};
    var jpipFactory = jpipRuntimeFactory;

    var databinsSaver = jpipFactory.createDatabinsSaver(/*isJpipTilepartStream=*/false);
    var mainHeaderDatabin = databinsSaver.getMainHeaderDatabin();

    var markersParser = jpipFactory.createMarkersParser(mainHeaderDatabin);
    var offsetsCalculator = jpipFactory.createOffsetsCalculator(
        mainHeaderDatabin, markersParser);
    var structureParser = jpipFactory.createStructureParser(
        databinsSaver, markersParser, offsetsCalculator);
    
    var progressionOrder = 'RPCL';
    var codestreamStructure = jpipFactory.createCodestreamStructure(
        structureParser, progressionOrder);
    
    var qualityLayersCache = jpipFactory.createQualityLayersCache(
        codestreamStructure);
        
    var headerModifier = jpipFactory.createHeaderModifier(
        codestreamStructure, offsetsCalculator, progressionOrder);
    var reconstructor = jpipFactory.createCodestreamReconstructor(
        codestreamStructure, databinsSaver, headerModifier, qualityLayersCache);
    var packetsDataCollector = jpipFactory.createPacketsDataCollector(
        codestreamStructure, databinsSaver, qualityLayersCache);
    
    var maxChannelsInSession = options.maxChannelsInSession || 1;
    var maxRequestsWaitingForResponseInChannel =
        options.maxRequestsWaitingForResponseInChannel || 1;
        
    var requester = jpipFactory.createReconnectableRequester(
        maxChannelsInSession,
        maxRequestsWaitingForResponseInChannel,
        codestreamStructure,
        databinsSaver);
    
    var jpipObjectsForRequestContext = {
        requester: requester,
        reconstructor: reconstructor,
        packetsDataCollector: packetsDataCollector,
        qualityLayersCache: qualityLayersCache,
        codestreamStructure: codestreamStructure,
        databinsSaver: databinsSaver,
        jpipFactory: jpipFactory
        };
    
    var statusCallback = null;
    
    this.setStatusCallback = function setStatusCallbackClosure(callback) {
        statusCallback = callback;
        
        if (callback !== null) {
            requester.setStatusCallback(requesterStatusCallback);
        } else {
            requester.setStatusCallback(null);
        }
    };
    
    this.open = function open(baseUrl) {
        requester.open(baseUrl);
    }
    
    this.close = function close(closedCallback) {
        requester.close(closedCallback);
    };
    
    this.getSizesParams = function getSizesParams() {
        if (!requester.getIsReady()) {
            throw new jpipExceptions.IllegalOperationException(
                'Cannot get codestream structure before image is ready');
        }
        
        var params = codestreamStructure.getSizesParams();
        var clonedParams = JSON.parse(JSON.stringify(params));
        
        var tile = codestreamStructure.getDefaultTileStructure();
        var component = tile.getDefaultComponentStructure();

        clonedParams.defaultNumQualityLayers =
            tile.getNumQualityLayers();
        clonedParams.defaultNumResolutionLevels =
            component.getNumResolutionLevels();
        
        return clonedParams;
    };
    
    this.createDataRequest = function createDataRequest(
        codestreamPartParams, callback, userContextVars, options) {
        
        options = options || {};
        if (options.isOnlyWaitForData !== undefined) {
            throw new jpipExceptions.ArgumentException(
                'options.isOnlyWaitForData',
                options.isOnlyWaitForData,
                'isOnlyWaitForData is supported only for progressive request');
        }
        
        var codestreamPartParamsModified = castCodestreamPartParams(
            codestreamPartParams);
        
        var progressiveness;
        if (options.useCachedDataOnly) {
            progressiveness = [ { minNumQualityLayers: 0 } ];
        } else {
            var maxNumQualityLayers = codestreamPartParams.maxNumQualityLayers;
            var minNumQualityLayers =
                maxNumQualityLayers === undefined ? 'max' : maxNumQualityLayers;
            
            progressiveness = [ { minNumQualityLayers: minNumQualityLayers } ];
        }
        
        var requestContext = jpipFactory.createRequestContext(
            jpipObjectsForRequestContext,
            codestreamPartParamsModified,
            callback,
            progressiveness,
            {
                disableServerRequests: !!options.useCachedDataOnly,
                isMovable: false,
                userContextVars: userContextVars,
                failureCallback: options.failureCallback
            });
        
        return requestContext;
    };
    
    this.createProgressiveDataRequest = function createProgressiveDataRequest(
        codestreamPartParams,
        callback,
        userContextVars,
        options,
        progressiveness) {
        
        options = options || {};
        if (options.useCachedDataOnly !== undefined) {
            throw new jpipExceptions.ArgumentException(
                'options.useCachedDataOnly',
                options.useCachedDataOnly,
                'useCachedDataOnly is not supported for progressive request');
        }
        
        var codestreamPartParamsModified = castCodestreamPartParams(
            codestreamPartParams);
        
        var progressivenessModified;
        if (progressiveness === undefined) {
            progressivenessModified = getAutomaticProgressivenessStages(
                codestreamPartParamsModified.maxNumQualityLayers);
        } else {
            progressivenessModified = castProgressivenessParams(
                progressiveness, codestreamPartParamsModified.maxNumQualityLayers);
        }
        
        var requestContext = jpipFactory.createRequestContext(
            jpipObjectsForRequestContext,
            codestreamPartParamsModified,
            callback,
            progressivenessModified,
            {
                disableServerRequests: !!options.isOnlyWaitForData,
                isMovable: false,
                userContextVars: userContextVars,
                failureCallback: options.failureCallback
            });
        
        return requestContext;
    }
    
    this.createMovableRequest = function createMovableRequest(
        callback, userContextVars) {
        
        // NOTE: Think of the correct API of progressiveness in movable requests
        
        var zombieCodestreamPartParams = null;
        var progressiveness = getAutomaticProgressivenessStages();
        
        var requestContext = jpipFactory.createRequestContext(
            jpipObjectsForRequestContext,
            zombieCodestreamPartParams,
            callback,
            progressiveness,
            {
                disableServerRequests: false,
                isMovable: true,
                userContextVars: userContextVars
            });
            
        return requestContext;
    };
    
    this.reconnect = function reconnect() {
        requester.reconnect();
    };
    
    function requesterStatusCallback(requesterStatus) {
        var serializableException = null;
        if (requesterStatus.exception !== null) {
            serializableException = requesterStatus.exception.toString();
        }
        
        var status = {
            isReady: requesterStatus.isReady,
            exception: serializableException
            };
        
        statusCallback(status);
    }
    
    function castProgressivenessParams(progressiveness, maxNumQualityLayers) {
        // Ensure than minNumQualityLayers is given for all items
        
        var result = new Array(progressiveness.length);

        for (var i = 0; i < progressiveness.length; ++i) {
            var minNumQualityLayers = progressiveness[i].minNumQualityLayers;
            
            if (minNumQualityLayers !== 'max') {
                if (maxNumQualityLayers !== undefined &&
                    minNumQualityLayers > maxNumQualityLayers) {
                    
                    throw new ArgumentException(
                        'progressiveness[' + i + '].minNumQualityLayers',
                        minNumQualityLayers,
                        'minNumQualityLayers is bigger than ' +
                            'fetchParams.maxNumQualityLayers');
                }
                
                minNumQualityLayers = validateNumericParam(
                    minNumQualityLayers,
                    propertyName,
                    'progressiveness[' + i + '].minNumQualityLayers');
            }
            
            result[i] = { minNumQualityLayers: minNumQualityLayers };
        }
        
        return result;
    }
    
    function getAutomaticProgressivenessStages(maxNumQualityLayers) {
        // Create progressiveness of (1, 2, 3, (#max-quality/2), (#max-quality))

        var progressiveness = [];

        // No progressiveness, wait for all quality layers to be fetched
        var tileStructure = codestreamStructure.getDefaultTileStructure();
        var numQualityLayersNumeric = tileStructure.getNumQualityLayers();
        var maxNumQualityLayersNumericOrMax = 'max';
        
        if (maxNumQualityLayers !== undefined) {
            numQualityLayersNumeric = Math.min(
                numQualityLayersNumeric, maxNumQualityLayers);
            maxNumQualityLayersNumericOrMax = numQualityLayersNumeric;
        }
        
        var firstQualityLayersCount = numQualityLayersNumeric < 4 ?
            numQualityLayersNumeric - 1: 3;
        
        for (var i = 1; i < firstQualityLayersCount; ++i) {
            progressiveness.push({ minNumQualityLayers: i });
        }
        
        var middleQuality = Math.round(numQualityLayersNumeric / 2);
        if (middleQuality > firstQualityLayersCount) {
            progressiveness.push({ minNumQualityLayers: middleQuality });
        }
        
        progressiveness.push({
            minNumQualityLayers: maxNumQualityLayersNumericOrMax
            });
        
        return progressiveness;
    }
    
    function castCodestreamPartParams(codestreamPartParams) {
        var numResolutionLevelsToCut = validateNumericParam(
            codestreamPartParams.numResolutionLevelsToCut,
            'numResolutionLevelsToCut',
            /*defaultValue=*/undefined,
            /*allowUndefiend=*/true);

        var maxNumQualityLayers = validateNumericParam(
            codestreamPartParams.maxNumQualityLayers,
            'maxNumQualityLayers',
            /*defaultValue=*/undefined,
            /*allowUndefiend=*/true);
        
        var minX = validateNumericParam(codestreamPartParams.minX, 'minX');
        var minY = validateNumericParam(codestreamPartParams.minY, 'minY');
        
        var maxX = validateNumericParam(
            codestreamPartParams.maxXExclusive, 'maxXExclusive');
        
        var maxY = validateNumericParam(
            codestreamPartParams.maxYExclusive, 'maxYExclusive');
        
        var levelWidth = codestreamStructure.getLevelWidth(numResolutionLevelsToCut)
        var levelHeight = codestreamStructure.getLevelHeight(numResolutionLevelsToCut)
        
        if (minX < 0 || maxX > levelWidth ||
            minY < 0 || maxY > levelHeight ||
            minX >= maxX || minY >= maxY) {
            
            throw new jpipExceptions.ArgumentException(
                'codestreamPartParams', codestreamPartParams);
        }
        
        var result = {
            minX: minX,
            minY: minY,
            maxXExclusive: maxX,
            maxYExclusive: maxY,
            
            numResolutionLevelsToCut: numResolutionLevelsToCut,
            maxNumQualityLayers: maxNumQualityLayers
            };
        
        return result;
    }
    
    function validateNumericParam(
        inputValue, propertyName, defaultValue, allowUndefined) {
        
        if (inputValue === undefined &&
            (defaultValue !== undefined || allowUndefined)) {
            
            return defaultValue;
        }
        
        var result = +inputValue;
        if (isNaN(result) || result !== Math.floor(result)) {
            throw new jpipExceptions.ArgumentException(
                propertyName, inputValue);
        }
        
        return result;
    }
    
    return this;
};