import Locator from '../services/Locator';
import { State } from '@popiplay/state-machine';

const DEFAULT_VOLUME = 0.7;
const LAST_VOLUME_STORAGE_KEY = 'lastVolume';
const VOLUME_STATE_STORAGE_KEY = 'volumeSystemState';

type VolumeAudioControls = {
    volumeAll: number;
    muteAll: boolean;
    muteMusic: boolean;
    muteSFX: boolean;
};

type StorageLike = {
    get<T = unknown>(key: string): T | null | undefined;
    set(key: string, value: unknown): void;
};

export enum VolumeStateType
    {
    UNMUTED = 0,
    MUSIC_MUTED = 1,
    SOUNDS_MUTED = 2,
    MUTED = 3,
}

/** Finite-state volume controller that synchronises Locator audio mutes and master volume. */
export default class VolumeModel
{
    private readonly storage: StorageLike;

    private readonly states: VolumeStateBase[];

    private currentState: VolumeStateBase;

    private lastVolume: number;

    private readonly updateLoop = (): void =>
    {
        this.currentState.handleInput();
        requestAnimationFrame(this.updateLoop);
    };

    constructor()
    {
        this.storage = Locator.storage as StorageLike;
        this.lastVolume = this.restoreVolume();
        this.states = [
            new UnmutedState(this),
            new MusicMutedState(this),
            new SoundsMutedState(this),
            new MutedState(this),
        ];
        this.currentState = this.states[VolumeStateType.UNMUTED];

        const storedState = this.storage.get<number>(VOLUME_STATE_STORAGE_KEY);

        this.audio.volumeAll = this.lastVolume;
        this.setState(this.resolveState(storedState));
        this.updateLoop();
    }

    /**
     * Persist and activate a specific volume state.
     * @param state
     */
    public setState(state: VolumeStateType): void
    {
        const nextState = this.states[state];

        if (!nextState)
        {
            return;
        }

        this.currentState = nextState;
        this.currentState.enter();
        this.storage.set(VOLUME_STATE_STORAGE_KEY, state);
    }

    public setMusicOn(): void
    {
        this.audio.muteMusic = false;
    }

    public setMusicOff(): void
    {
        this.audio.muteMusic = true;
    }

    /** Toggle the music mute flag. */
    public toggleMusic(): void
    {
        this.audio.muteMusic = !this.audio.muteMusic;
    }

    public isMusicMuted(): boolean
    {
        return Boolean(this.audio.muteMusic);
    }

    public setSoundsOn(): void
    {
        this.audio.muteSFX = false;
    }

    public setSoundsOff(): void
    {
        this.audio.muteSFX = true;
    }

    /** Toggle the sound effects mute flag. */
    public toggleSounds(): void
    {
        this.audio.muteSFX = !this.audio.muteSFX;
    }

    public isSoundsMuted(): boolean
    {
        return Boolean(this.audio.muteSFX);
    }

    public getVolume(): number
    {
        return this.audio.volumeAll;
    }

    /**
     * Updates master volume and synchronises mute-all flag.
     * @param value New master volume value.
     */
    public setVolume(value: number): void
    {
        this.audio.volumeAll = value;

        if (value <= 0)
        {
            this.audio.volumeAll = 0;
            this.muteAll();
        }
        else
        {
            this.unmuteAll();
        }
    }

    public saveVolume(): void
    {
        const currentVolume = this.getVolume();

        if (currentVolume > 0)
        {
            this.lastVolume = currentVolume;
            this.storage.set(LAST_VOLUME_STORAGE_KEY, currentVolume);
        }
    }

    public isAllMuted(): boolean
    {
        return Boolean(this.audio.muteAll);
    }

    public muteAll(): void
    {
        this.audio.muteAll = true;
    }

    public unmuteAll(): void
    {
        this.audio.muteAll = false;
    }

    public getActualVolume(): number
    {
        const currentVolume = this.getVolume();

        if (currentVolume === 0)
        {
            return this.lastVolume;
        }

        return currentVolume;
    }

    private get audio(): VolumeAudioControls
    {
        return Locator.audio as VolumeAudioControls;
    }

    private restoreVolume(): number
    {
        const storedVolume = this.storage.get<number>(LAST_VOLUME_STORAGE_KEY);

        return typeof storedVolume === 'number' ? storedVolume : DEFAULT_VOLUME;
    }

    private resolveState(value: unknown): VolumeStateType
    {
        if (this.isValidState(value))
        {
            return value;
        }

        return VolumeStateType.UNMUTED;
    }

    private isValidState(value: unknown): value is VolumeStateType
    {
        return typeof value === 'number' && value >= 0 && value < this.states.length;
    }
}

abstract class VolumeStateBase extends State
{
    protected readonly system: VolumeModel;

    constructor(name: string, system: VolumeModel)
    {
        super(name);
        this.system = system;
    }
}

class UnmutedState extends VolumeStateBase
{
    constructor(system: VolumeModel)
    {
        super('UnmutedState', system);
    }

    public override enter(): void
    {
        super.enter();
        this.system.unmuteAll();
        this.system.setVolume(this.system.getActualVolume());
        this.system.setMusicOn();
        this.system.setSoundsOn();
    }

    public override handleInput(): void
    {
        if (this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.getVolume() === 0)
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.isSoundsMuted())
        {
            this.system.setState(VolumeStateType.SOUNDS_MUTED);
        }
        else if (this.system.isMusicMuted())
        {
            this.system.setState(VolumeStateType.MUSIC_MUTED);
        }
    }
}

class MusicMutedState extends VolumeStateBase
{
    constructor(system: VolumeModel)
    {
        super('MusicMutedState', system);
    }

    public override enter(): void
    {
        super.enter();
        this.system.unmuteAll();
        this.system.setVolume(this.system.getActualVolume());
        this.system.setMusicOff();
        this.system.setSoundsOn();
    }

    public override handleInput(): void
    {
        if (this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.getVolume() === 0)
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.isSoundsMuted())
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (!this.system.isMusicMuted())
        {
            this.system.setState(VolumeStateType.UNMUTED);
        }
    }
}

class SoundsMutedState extends VolumeStateBase
{
    constructor(system: VolumeModel)
    {
        super('SoundsMutedState', system);
    }

    public override enter(): void
    {
        super.enter();
        this.system.unmuteAll();
        this.system.setMusicOn();
        this.system.setVolume(this.system.getActualVolume());
        this.system.setSoundsOff();
    }

    public override handleInput(): void
    {
        if (this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.getVolume() === 0)
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (this.system.isMusicMuted())
        {
            this.system.setState(VolumeStateType.MUTED);
        }
        else if (!this.system.isSoundsMuted())
        {
            this.system.setState(VolumeStateType.UNMUTED);
        }
    }
}

class MutedState extends VolumeStateBase
{
    private lastMusicState = true;

    private lastSoundsState = true;

    constructor(system: VolumeModel)
    {
        super('MutedState', system);
    }

    public override enter(): void
    {
        super.enter();
        this.lastMusicState = this.system.isMusicMuted();
        this.lastSoundsState = this.system.isSoundsMuted();
        this.system.muteAll();
        this.system.setVolume(0);
        this.system.setMusicOff();
        this.system.setSoundsOff();
    }

    public override handleInput(): void
    {
        if (!this.lastMusicState && !this.lastSoundsState && !this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.UNMUTED);
        }
        else if (!this.lastMusicState && !this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.SOUNDS_MUTED);
        }
        else if (!this.lastSoundsState && !this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.MUSIC_MUTED);
        }
        else if (this.lastMusicState && this.lastSoundsState && !this.system.isAllMuted())
        {
            this.system.setState(VolumeStateType.UNMUTED);
        }
        else if (this.system.getVolume() > 0)
        {
            this.system.setState(VolumeStateType.MUSIC_MUTED);
        }
        else if (!this.system.isMusicMuted())
        {
            this.system.setState(VolumeStateType.SOUNDS_MUTED);
        }
        else if (!this.system.isSoundsMuted())
        {
            this.system.setState(VolumeStateType.MUSIC_MUTED);
        }
    }
}
