import { AvatarAction, AvatarDirectionAngle, AvatarScaleType, AvatarSetType, IActiveActionData, IAnimationLayerData, IAvatarDataContainer, IAvatarEffectListener, IAvatarFigureContainer, IAvatarImage, IPartColor, ISpriteDataContainer } from '@nitrots/api';
import { GetRenderer, GetTexturePool, GetTickerTime, PaletteMapFilter, TextureUtils } from '@nitrots/utils';
import { ColorMatrixFilter, Container, RenderTexture, Sprite, Texture } from 'pixi.js';
import { AvatarFigureContainer } from './AvatarFigureContainer';
import { AvatarStructure } from './AvatarStructure';
import { EffectAssetDownloadManager } from './EffectAssetDownloadManager';
import { ActiveActionData } from './actions';
import { AssetAliasCollection } from './alias';
import { AvatarImageCache } from './cache';
import { AvatarCanvas } from './structure';

export class AvatarImage implements IAvatarImage, IAvatarEffectListener
{
    private static CHANNELS_EQUAL: string = 'CHANNELS_EQUAL';
    private static CHANNELS_UNIQUE: string = 'CHANNELS_UNIQUE';
    private static CHANNELS_RED: string = 'CHANNELS_RED';
    private static CHANNELS_GREEN: string = 'CHANNELS_GREEN';
    private static CHANNELS_BLUE: string = 'CHANNELS_BLUE';
    private static CHANNELS_DESATURATED: string = 'CHANNELS_DESATURATED';
    private static DEFAULT_ACTION: string = 'Default';
    private static DEFAULT_DIRECTION: number = 2;
    private static DEFAULT_AVATAR_SET: string = AvatarSetType.FULL;

    protected _mainDirection: number;
    protected _headDirection: number;
    protected _mainAction: IActiveActionData;
    protected _disposed: boolean = false;
    protected _canvasOffsets: number[] = [];
    protected _cache: AvatarImageCache;
    protected _avatarSpriteData: IAvatarDataContainer;
    protected _actions: ActiveActionData[] = [];
    protected _activeTexture: Texture = null;

    private _defaultAction: IActiveActionData = null;
    private _frameCounter: number = 0;
    private _directionOffset: number = 0;
    private _changes: boolean = true;
    private _sprites: ISpriteDataContainer[];
    private _isAnimating: boolean = false;
    private _animationHasResetOnToggle: boolean = false;
    private _actionsSorted: boolean = false;
    private _sortedActions: IActiveActionData[];
    private _lastActionsString: string = null;
    private _currentActionsString: string = null;
    private _effectIdInUse: number = -1;
    private _animationFrameCount: number = -1;
    private _cachedBodyParts: string[] = [];
    private _cachedBodyPartsDirection: number = -1;
    private _cachedBodyPartsGeometryType: string = null;
    private _cachedBodyPartsAvatarSet: string = null;

    constructor(
        private _structure: AvatarStructure,
        private _assets: AssetAliasCollection,
        private _figure: AvatarFigureContainer,
        private _scale: string,
        private _effectManager: EffectAssetDownloadManager,
        private _effectListener: IAvatarEffectListener = null)
    {
        if(!this._figure) this._figure = new AvatarFigureContainer('hr-893-45.hd-180-2.ch-210-66.lg-270-82.sh-300-91.wa-2007-.ri-1-');
        if(!this._scale) this._scale = AvatarScaleType.LARGE;

        this._cache = new AvatarImageCache(this._structure, this, this._assets, this._scale);
        this.setDirection(AvatarImage.DEFAULT_AVATAR_SET, AvatarImage.DEFAULT_DIRECTION);
        this._defaultAction = new ActiveActionData(AvatarAction.POSTURE_STAND);
        this._defaultAction.definition = this._structure.getActionDefinition(AvatarImage.DEFAULT_ACTION);
        this.resetActions();
        this._animationFrameCount = 0;
    }

    public dispose(): void
    {
        if(this._disposed) return;

        this._structure = null;
        this._assets = null;
        this._mainAction = null;
        this._figure = null;
        this._avatarSpriteData = null;
        this._actions = null;

        if(this._activeTexture)
        {
            GetTexturePool().putTexture(this._activeTexture);

            this._activeTexture = null;
        }

        if(this._cache)
        {
            this._cache.dispose();
            this._cache = null;
        }

        this._canvasOffsets = null;
        this._disposed = true;
    }

    public get disposed(): boolean
    {
        return this._disposed;
    }

    public getFigure(): IAvatarFigureContainer
    {
        return this._figure;
    }

    public getScale(): string
    {
        return this._scale;
    }

    public getPartColor(k: string): IPartColor
    {
        return this._structure.getPartColor(this._figure, k);
    }

    public setDirection(avatarPart: string, direction: number): void
    {
        direction += this._directionOffset;

        if(direction < AvatarDirectionAngle.MIN_DIRECTION)
        {
            direction = AvatarDirectionAngle.MAX_DIRECTION + (direction + 1);
        }
        else if(direction > AvatarDirectionAngle.MAX_DIRECTION)
        {
            direction -= (AvatarDirectionAngle.MAX_DIRECTION + 1);
        }

        if(this._structure.isMainAvatarSet(avatarPart)) this._mainDirection = direction;

        // Special handling for head direction, including prevention checks for turning
        if(avatarPart === AvatarSetType.HEAD || avatarPart === AvatarSetType.FULL)
        {
            if(avatarPart === AvatarSetType.HEAD && this.isHeadTurnPreventedByAction()) direction = this._mainDirection;

            this._headDirection = direction;
        }

        this._cache.setDirection(avatarPart, direction);
        this._changes = true;
    }

    public setDirectionAngle(k: string, _arg_2: number): void
    {
        this.setDirection(k, Math.floor(_arg_2 / 45));
    }

    public getSprites(): ISpriteDataContainer[]
    {
        return this._sprites;
    }

    public getCanvasOffsets(): number[]
    {
        return this._canvasOffsets;
    }

    public getMainAction(): IActiveActionData
    {
        return this._mainAction;
    }

    public getLayerData(k: ISpriteDataContainer): IAnimationLayerData
    {
        return this._structure.getBodyPartData(k.animation.id, this._frameCounter, k.id);
    }

    public updateAnimationByFrames(k: number = 1): void
    {
        this._frameCounter += k;
        this._changes = true;
    }

    public resetAnimationFrameCounter(): void
    {
        this._frameCounter = 0;
        this._changes = true;
    }

    private getBodyParts(avatarSet: string, geometryType: string, direction: number): string[]
    {
        const shouldUpdateCache = direction !== this._cachedBodyPartsDirection || geometryType !== this._cachedBodyPartsGeometryType || avatarSet !== this._cachedBodyPartsAvatarSet;

        if(shouldUpdateCache)
        {
            this._cachedBodyPartsDirection = direction;
            this._cachedBodyPartsGeometryType = geometryType;
            this._cachedBodyPartsAvatarSet = avatarSet;

            this._cachedBodyParts = this._structure.getBodyParts(avatarSet, geometryType, direction);
        }

        return this._cachedBodyParts;
    }

    private buildAvatarContainer(avatarCanvas: AvatarCanvas, setType: string): Container
    {
        const bodyParts = this.getBodyParts(setType, this._mainAction.definition.geometryType, this._mainDirection);
        const container = new Container();

        let partCount = (bodyParts.length - 1);

        while(partCount >= 0)
        {
            const set = bodyParts[partCount];
            const part = this._cache.getImageContainer(set, this._frameCounter);

            if(part)
            {
                const partCacheContainer = part.image;

                if(partCacheContainer)
                {
                    const partContainer = new Container();

                    partContainer.addChild(partCacheContainer);

                    const point = part.regPoint.clone();

                    point.x += avatarCanvas.offset.x;
                    point.y += avatarCanvas.offset.y;

                    point.x += avatarCanvas.regPoint.x;
                    point.y += avatarCanvas.regPoint.y;

                    partContainer.x = Math.floor(point.x);
                    partContainer.y = Math.floor(point.y);

                    container.addChild(partContainer);
                }
            }

            partCount--;
        }

        container.filters = [];

        if(this._avatarSpriteData)
        {
            if(this._avatarSpriteData.colorTransform)
            {
                if(container.filters === undefined || container.filters === null) container.filters = [ this._avatarSpriteData.colorTransform ];
                else if(Array.isArray(container.filters)) container.filters = [ ...container.filters, this._avatarSpriteData.colorTransform ];
                else container.filters = [ container.filters, this._avatarSpriteData.colorTransform ];
            }

            if(this._avatarSpriteData.paletteIsGrayscale)
            {
                this.convertToGrayscale(container);

                const paletteMapFilter = new PaletteMapFilter({
                    palette: this._avatarSpriteData.reds,
                    channel: PaletteMapFilter.CHANNEL_RED
                });

                if(container.filters === undefined || container.filters === null) container.filters = [ paletteMapFilter ];
                else if(Array.isArray(container.filters)) container.filters = [ ...container.filters, paletteMapFilter ];
                else container.filters = [ container.filters, paletteMapFilter ];
            }
        }

        return container;
    }

    public processAsTexture(setType: string, hightlight: boolean): Texture
    {
        if(!this._changes) return this._activeTexture;

        if(!this._mainAction) return null;

        if(!this._actionsSorted) this.endActionAppends();

        const avatarCanvas = this._structure.getCanvas(this._scale, this._mainAction.definition.geometryType);

        if(!avatarCanvas) return null;

        if(this._activeTexture && ((this._activeTexture.width !== avatarCanvas.width) || (this._activeTexture.height !== avatarCanvas.height)))
        {
            GetTexturePool().putTexture(this._activeTexture);

            this._activeTexture = null;
        }

        if(!this._activeTexture) this._activeTexture = GetTexturePool().getTexture(avatarCanvas.width, avatarCanvas.height);

        if(!this._activeTexture) return null;

        const container = this.buildAvatarContainer(avatarCanvas, setType);

        if(!container) return null;

        GetRenderer().render({
            target: this._activeTexture,
            container: container,
            clear: true
        });

        for(const child of container.children)
        {
            child.removeChildren();
        }

        container.destroy({ children: true });

        //@ts-ignore
        this._activeTexture.source.hitMap = null;

        this._changes = false;

        return this._activeTexture;
    }

    public processAsImageUrl(setType: string, scale: number = 1): string
    {
        const texture = this.processAsTexture(setType, false);
        const canvas = GetRenderer().texture.generateCanvas(texture);

        const url = canvas.toDataURL('image/png');
		
		canvas.width = 0;
        canvas.height = 0;

        return url;
    }

    public processAsContainer(setType: string): Container
    {
        if(!this._mainAction) return null;

        if(!this._actionsSorted) this.endActionAppends();

        const avatarCanvas = this._structure.getCanvas(this._scale, this._mainAction.definition.geometryType);

        if(!avatarCanvas) return null;

        return this.buildAvatarContainer(avatarCanvas, setType);
    }

    // TODO this needs to be added still
    public applyPalette(texture: RenderTexture, reds: number[] = [], greens: number[] = [], blues: number[] = []): RenderTexture
    {
        const textureCanvas = TextureUtils.generateCanvas(texture);
        const textureCtx = textureCanvas.getContext('2d');
        const textureImageData = textureCtx.getImageData(0, 0, textureCanvas.width, textureCanvas.height);
        const data = textureImageData.data;

        for(let i = 0; i < data.length; i += 4)
        {
            if(reds.length == 256)
            {
                let paletteColor = reds[data[i]];
                if(paletteColor === undefined) paletteColor = 0;

                data[i] = ((paletteColor >> 16) & 0xFF);
                data[i + 1] = ((paletteColor >> 8) & 0xFF);
                data[i + 2] = (paletteColor & 0xFF);
            }

            if(greens.length == 256)
            {
                let paletteColor = greens[data[i + 1]];
                if(paletteColor === undefined) paletteColor = 0;

                data[i] = ((paletteColor >> 16) & 0xFF);
                data[i + 1] = ((paletteColor >> 8) & 0xFF);
                data[i + 2] = (paletteColor & 0xFF);
            }
            if(blues.length == 256)
            {
                let paletteColor = greens[data[i + 2]];
                if(paletteColor === undefined) paletteColor = 0;

                data[i] = ((paletteColor >> 16) & 0xFF);
                data[i + 1] = ((paletteColor >> 8) & 0xFF);
                data[i + 2] = (paletteColor & 0xFF);
            }
        }

        textureCtx.putImageData(textureImageData, 0, 0);

        const newTexture = new Sprite(Texture.from(textureCanvas));

        TextureUtils.writeToTexture(newTexture, texture, true);

        return texture;
    }


    public getAsset(name: string): IGraphicAsset
    {
        return this._assets.getAsset(name);
    }

    public getDirection(): number
    {
        return this._mainDirection;
    }

    public getDirectionOffset(): number
    {
        return this._directionOffset;
    }

    public initActionAppends(): void
    {
        this._actions = [];
        this._actionsSorted = false;
        this._currentActionsString = '';
    }

    public endActionAppends(): void
    {
        if(!this.sortActions()) return;

        for(const k of this._sortedActions)
        {
            if(k.actionType === AvatarAction.EFFECT)
            {
                if(!this._effectManager.isAvatarEffectReady(parseInt(k.actionParameter))) this._effectManager.downloadAvatarEffect(parseInt(k.actionParameter), this);
            }
        }

        this.resetActions();
        this.setActionsToParts();
    }

    public appendAction(k: string, ..._args: any[]): boolean
    {
        let _local_3 = '';

        this._actionsSorted = false;

        if(_args && (_args.length > 0)) _local_3 = _args[0];

        if((_local_3 !== undefined) && (_local_3 !== null)) _local_3 = _local_3.toString();

        switch(k)
        {
            case AvatarAction.POSTURE:
                switch(_local_3)
                {
                    case AvatarAction.POSTURE_LAY:
                    case AvatarAction.POSTURE_WALK:
                    case AvatarAction.POSTURE_STAND:
                    case AvatarAction.POSTURE_SWIM:
                    case AvatarAction.POSTURE_FLOAT:
                    case AvatarAction.POSTURE_SIT:
                    case AvatarAction.SNOWWAR_RUN:
                    case AvatarAction.SNOWWAR_DIE_FRONT:
                    case AvatarAction.SNOWWAR_DIE_BACK:
                    case AvatarAction.SNOWWAR_PICK:
                    case AvatarAction.SNOWWAR_THROW:
                        if((_local_3 === AvatarAction.POSTURE_LAY) || (_local_3 === AvatarAction.POSTURE_LAY) || (_local_3 === AvatarAction.POSTURE_LAY))
                        {
                            if(_local_3 === AvatarAction.POSTURE_LAY)
                            {
                                if(this._mainDirection == 0)
                                {
                                    this.setDirection(AvatarSetType.FULL, 4);
                                }
                                else
                                {
                                    this.setDirection(AvatarSetType.FULL, 2);
                                }
                            }
                        }

                        this.addActionData(_local_3);
                        break;
                }
                break;
            case AvatarAction.GESTURE:
                switch(_local_3)
                {
                    case AvatarAction.GESTURE_AGGRAVATED:
                    case AvatarAction.GESTURE_SAD:
                    case AvatarAction.GESTURE_SMILE:
                    case AvatarAction.GESTURE_SURPRISED:
                        this.addActionData(_local_3);
                        break;
                }
                break;
            case AvatarAction.EFFECT:
            case AvatarAction.DANCE:
            case AvatarAction.TALK:
            case AvatarAction.EXPRESSION_WAVE:
            case AvatarAction.SLEEP:
            case AvatarAction.BLINK:
            case AvatarAction.SIGN:
            case AvatarAction.EXPRESSION_RESPECT:
            case AvatarAction.EXPRESSION_BLOW_A_KISS:
            case AvatarAction.EXPRESSION_LAUGH:
            case AvatarAction.EXPRESSION_CRY:
            case AvatarAction.EXPRESSION_IDLE:
            case AvatarAction.EXPRESSION_SNOWBOARD_OLLIE:
            case AvatarAction.EXPRESSION_SNOWBORD_360:
            case AvatarAction.EXPRESSION_RIDE_JUMP:
                if(_local_3 === AvatarAction.EFFECT)
                {
                    if((((((_local_3 === '33') || (_local_3 === '34')) || (_local_3 === '35')) || (_local_3 === '36')) || (_local_3 === '38')) || (_local_3 === '39'))
                    {
                        //
                    }
                }

                this.addActionData(k, _local_3);
                break;
            case AvatarAction.CARRY_OBJECT:
            case AvatarAction.USE_OBJECT: {
                const _local_4 = this._structure.getActionDefinitionWithState(k);
                if(_local_4) _local_3 = _local_4.getParameterValue(_local_3);
                this.addActionData(k, _local_3);
                break;
            }
        }

        return true;
    }

    protected addActionData(actionType: string, actionParameter: string = ''): void
    {
        if(!this._actions) this._actions = [];

        const actionExists = this._actions.some(action =>
            action.actionType === actionType && action.actionParameter === actionParameter
        );

        if(!actionExists) this._actions.push(new ActiveActionData(actionType, actionParameter, this._frameCounter));
    }

    public isAnimating(): boolean
    {
        return (this._isAnimating) || (this._animationFrameCount > 1);
    }

    private resetActions(): boolean
    {
        this._animationHasResetOnToggle = false;
        this._isAnimating = false;
        this._sprites = [];
        this._avatarSpriteData = null;
        this._directionOffset = 0;
        this._structure.removeDynamicItems(this);
        this._mainAction = this._defaultAction;
        this._mainAction.definition = this._defaultAction.definition;
        this.resetBodyPartCache(this._defaultAction);
        return true;
    }

    private isHeadTurnPreventedByAction(): boolean
    {
        if(!this._sortedActions) return false;

        for(const action of this._sortedActions)
        {
            const actionDefinition = this._structure.getActionDefinitionWithState(action.actionType);

            if(actionDefinition != null && actionDefinition.getPreventHeadTurn(action.actionParameter)) return true;
        }

        return false;
    }

    private sortActions(): boolean
    {
        let hasChanges = false;
        let hasEffectAction = false;
        let effectChanged = false;

        this._currentActionsString = '';
        this._sortedActions = this._structure.sortActions(this._actions);
        this._animationFrameCount = this._structure.maxFrames(this._sortedActions);

        if(!this._sortedActions)
        {
            this._canvasOffsets = [0, 0, 0];

            if(this._lastActionsString !== '')
            {
                hasChanges = true;

                this._lastActionsString = '';
            }
        }
        else
        {
            this._canvasOffsets = this._structure.getCanvasOffsets(this._sortedActions, this._scale, this._mainDirection);

            for(const action of this._sortedActions)
            {
                this._currentActionsString += action.actionType + action.actionParameter;

                if(action.actionType === AvatarAction.EFFECT)
                {
                    const effectId = parseInt(action.actionParameter);

                    if(this._effectIdInUse !== effectId) effectChanged = true;

                    this._effectIdInUse = effectId;

                    hasEffectAction = true;
                }
            }

            if(!hasEffectAction)
            {
                if(this._effectIdInUse > -1) effectChanged = true;

                this._effectIdInUse = -1;
            }

            if(effectChanged) this._cache.disposeInactiveActions(0);

            if(this._lastActionsString != this._currentActionsString)
            {
                hasChanges = true;

                this._lastActionsString = this._currentActionsString;
            }
        }

        this._actionsSorted = true;

        return hasChanges;
    }

    private setActionsToParts(): void
    {
        if(!this._sortedActions) return;

        const currentTime = GetTickerTime();
        const actionTypes: string[] = [];

        for(const action of this._sortedActions) actionTypes.push(action.actionType);

        for(const action of this._sortedActions)
        {
            if(action && action.definition && action.definition.isAnimation)
            {
                const animation = this._structure.getAnimation(`${action.definition.state}.${action.actionParameter}`);

                if(animation && animation.hasOverriddenActions())
                {
                    const overriddenActionNames = animation.overriddenActionNames();

                    if(overriddenActionNames)
                    {
                        for(const overriddenActionName of overriddenActionNames)
                        {
                            if(actionTypes.includes(overriddenActionName)) action.overridingAction = animation.overridingAction(overriddenActionName);
                        }
                    }
                }

                if(animation && animation.resetOnToggle) this._animationHasResetOnToggle = true;
            }
        }

        for(const action of this._sortedActions)
        {
            if(action && action.definition)
            {
                if(action.definition.isAnimation && action.actionParameter === '') action.actionParameter = '1';

                this.setActionToParts(action, currentTime);

                if(action.definition.isAnimation)
                {
                    this._isAnimating = action.definition.isAnimated(action.actionParameter);

                    const animation = this._structure.getAnimation(`${action.definition.state}.${action.actionParameter}`);

                    if(animation)
                    {
                        this._sprites = [...this._sprites, ...animation.spriteData];

                        if(animation.hasDirectionData()) this._directionOffset = animation.directionData.offset;

                        if(animation.hasAvatarData()) this._avatarSpriteData = animation.avatarData;

                        if(!this._isAnimating && (animation.spriteData?.length > 0 || animation.hasAvatarData()))
                        {
                            this._isAnimating = true;
                        }
                    }
                }
            }
        }
    }

    private setActionToParts(action: IActiveActionData, currentTime: number): void
    {
        if(!action || !action.definition || action.definition.assetPartDefinition === '') return;

        if(action.definition.isMain)
        {
            this._mainAction = action;
            this._cache.setGeometryType(action.definition.geometryType);
        }

        this._cache.setAction(action, currentTime);

        this._changes = true;
    }

    private resetBodyPartCache(action: IActiveActionData): void
    {
        if(!action || action.definition.assetPartDefinition === '') return;

        if(action.definition.isMain)
        {
            this._mainAction = action;
            this._cache.setGeometryType(action.definition.geometryType);
        }

        this._cache.resetBodyPartCache(action);

        this._changes = true;
    }

    private convertToGrayscale(container: Container, channel: string = 'CHANNELS_EQUAL'): Container
    {
        let redWeight = 0.33;
        let greenWeight = 0.33;
        let blueWeight = 0.33;

        switch(channel)
        {
            case AvatarImage.CHANNELS_UNIQUE:
                redWeight = 0.3;
                greenWeight = 0.59;
                blueWeight = 0.11;
                break;
            case AvatarImage.CHANNELS_RED:
                redWeight = 1;
                greenWeight = 0;
                blueWeight = 0;
                break;
            case AvatarImage.CHANNELS_GREEN:
                redWeight = 0;
                greenWeight = 1;
                blueWeight = 0;
                break;
            case AvatarImage.CHANNELS_BLUE:
                redWeight = 0;
                greenWeight = 0;
                blueWeight = 1;
                break;
            case AvatarImage.CHANNELS_DESATURATED:
                redWeight = 0.3086;
                greenWeight = 0.6094;
                blueWeight = 0.082;
                break;
        }

        const filter = new ColorMatrixFilter();

        filter.matrix = [
            redWeight, greenWeight, blueWeight, 0, 0, // Red channel
            redWeight, greenWeight, blueWeight, 0, 0, // Green channel
            redWeight, greenWeight, blueWeight, 0, 0, // Blue channel
            0, 0, 0, 1, 0 // Alpha channel
        ];

        if(container.filters === undefined || container.filters === null) container.filters = [ filter ];
        else if(Array.isArray(container.filters)) container.filters = [ ...container.filters, filter ];
        else container.filters = [ container.filters, filter ];

        return container;
    }

    public isPlaceholder(): boolean
    {
        return false;
    }

    public get animationHasResetOnToggle(): boolean
    {
        return this._animationHasResetOnToggle;
    }

    public resetEffect(effect: number): void
    {
        if(effect === this._effectIdInUse)
        {
            this.resetActions();
            this.setActionsToParts();

            this._animationHasResetOnToggle = true;
            this._changes = true;

            if(this._effectListener) this._effectListener.resetEffect(effect);
        }
    }
}
