import {Color} from '@maplibre/maplibre-gl-style-spec';
import {isWebGL2} from './webgl2';

import type {Context} from './context';
import type {
    BlendFuncType,
    BlendEquationType,
    ColorMaskType,
    DepthRangeType,
    DepthMaskType,
    StencilFuncType,
    StencilOpType,
    DepthFuncType,
    TextureUnitType,
    ViewportType,
    CullFaceModeType,
    FrontFaceType,
} from './types';

export interface IValue<T> {
    current: T;
    default: T;
    dirty: boolean;
    get(): T;
    setDefault(): void;
    set(value: T): void;
}

class BaseValue<T> implements IValue<T> {
    gl: WebGLRenderingContext|WebGL2RenderingContext;
    current: T;
    default: T;
    dirty: boolean;

    constructor(context: Context) {
        this.gl = context.gl;
        this.default = this.getDefault();
        this.current = this.default;
        this.dirty = false;
    }

    get(): T {
        return this.current;
    }
    set(value: T) { // eslint-disable-line
        // overridden in child classes;
    }

    getDefault(): T {
        return this.default; // overridden in child classes
    }
    setDefault() {
        this.set(this.default);
    }
}

export class ClearColor extends BaseValue<Color> {
    getDefault(): Color {
        return Color.transparent;
    }
    set(v: Color) {
        const c = this.current;
        if (v.r === c.r && v.g === c.g && v.b === c.b && v.a === c.a && !this.dirty) return;
        this.gl.clearColor(v.r, v.g, v.b, v.a);
        this.current = v;
        this.dirty = false;
    }
}

export class ClearDepth extends BaseValue<number> {
    getDefault(): number {
        return 1;
    }
    set(v: number) {
        if (v === this.current && !this.dirty) return;
        this.gl.clearDepth(v);
        this.current = v;
        this.dirty = false;
    }
}

export class ClearStencil extends BaseValue<number> {
    getDefault(): number {
        return 0;
    }
    set(v: number) {
        if (v === this.current && !this.dirty) return;
        this.gl.clearStencil(v);
        this.current = v;
        this.dirty = false;
    }
}

export class ColorMask extends BaseValue<ColorMaskType> {
    getDefault(): ColorMaskType {
        return [true, true, true, true];
    }
    set(v: ColorMaskType) {
        const c = this.current;
        if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && v[3] === c[3] && !this.dirty) return;
        this.gl.colorMask(v[0], v[1], v[2], v[3]);
        this.current = v;
        this.dirty = false;
    }
}

export class DepthMask extends BaseValue<DepthMaskType> {
    getDefault(): DepthMaskType {
        return true;
    }
    set(v: DepthMaskType): void {
        if (v === this.current && !this.dirty) return;
        this.gl.depthMask(v);
        this.current = v;
        this.dirty = false;
    }
}

export class StencilMask extends BaseValue<number> {
    getDefault(): number {
        return 0xFF;
    }
    set(v: number): void {
        if (v === this.current && !this.dirty) return;
        this.gl.stencilMask(v);
        this.current = v;
        this.dirty = false;
    }
}

export class StencilFunc extends BaseValue<StencilFuncType> {
    getDefault(): StencilFuncType {
        return {
            func: this.gl.ALWAYS,
            ref: 0,
            mask: 0xFF
        };
    }
    set(v: StencilFuncType): void {
        const c = this.current;
        if (v.func === c.func && v.ref === c.ref && v.mask === c.mask && !this.dirty) return;
        this.gl.stencilFunc(v.func, v.ref, v.mask);
        this.current = v;
        this.dirty = false;
    }
}

export class StencilOp extends BaseValue<StencilOpType> {
    getDefault(): StencilOpType {
        const gl = this.gl;
        return [gl.KEEP, gl.KEEP, gl.KEEP];
    }
    set(v: StencilOpType) {
        const c = this.current;
        if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && !this.dirty) return;
        this.gl.stencilOp(v[0], v[1], v[2]);
        this.current = v;
        this.dirty = false;
    }
}

export class StencilTest extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        if (v) {
            gl.enable(gl.STENCIL_TEST);
        } else {
            gl.disable(gl.STENCIL_TEST);
        }
        this.current = v;
        this.dirty = false;
    }
}

export class DepthRange extends BaseValue<DepthRangeType> {
    getDefault(): DepthRangeType {
        return [0, 1];
    }
    set(v: DepthRangeType) {
        const c = this.current;
        if (v[0] === c[0] && v[1] === c[1] && !this.dirty) return;
        this.gl.depthRange(v[0], v[1]);
        this.current = v;
        this.dirty = false;
    }
}

export class DepthTest extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        if (v) {
            gl.enable(gl.DEPTH_TEST);
        } else {
            gl.disable(gl.DEPTH_TEST);
        }
        this.current = v;
        this.dirty = false;
    }
}

export class DepthFunc extends BaseValue<DepthFuncType> {
    getDefault(): DepthFuncType {
        return this.gl.LESS;
    }
    set(v: DepthFuncType) {
        if (v === this.current && !this.dirty) return;
        this.gl.depthFunc(v);
        this.current = v;
        this.dirty = false;
    }
}

export class Blend extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        if (v) {
            gl.enable(gl.BLEND);
        } else {
            gl.disable(gl.BLEND);
        }
        this.current = v;
        this.dirty = false;
    }
}

export class BlendFunc extends BaseValue<BlendFuncType> {
    getDefault(): BlendFuncType {
        const gl = this.gl;
        return [gl.ONE, gl.ZERO];
    }
    set(v: BlendFuncType) {
        const c = this.current;
        if (v[0] === c[0] && v[1] === c[1] && !this.dirty) return;
        this.gl.blendFunc(v[0], v[1]);
        this.current = v;
        this.dirty = false;
    }
}

export class BlendColor extends BaseValue<Color> {
    getDefault(): Color {
        return Color.transparent;
    }
    set(v: Color) {
        const c = this.current;
        if (v.r === c.r && v.g === c.g && v.b === c.b && v.a === c.a && !this.dirty) return;
        this.gl.blendColor(v.r, v.g, v.b, v.a);
        this.current = v;
        this.dirty = false;
    }
}

export class BlendEquation extends BaseValue<BlendEquationType> {
    getDefault(): BlendEquationType {
        return this.gl.FUNC_ADD;
    }
    set(v: BlendEquationType) {
        if (v === this.current && !this.dirty) return;
        this.gl.blendEquation(v);
        this.current = v;
        this.dirty = false;
    }
}

export class CullFace extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        if (v) {
            gl.enable(gl.CULL_FACE);
        } else {
            gl.disable(gl.CULL_FACE);
        }
        this.current = v;
        this.dirty = false;
    }
}

export class CullFaceSide extends BaseValue<CullFaceModeType> {
    getDefault(): CullFaceModeType {
        return this.gl.BACK;
    }
    set(v: CullFaceModeType) {
        if (v === this.current && !this.dirty) return;
        this.gl.cullFace(v);
        this.current = v;
        this.dirty = false;
    }
}

export class FrontFace extends BaseValue<FrontFaceType> {
    getDefault(): FrontFaceType {
        return this.gl.CCW;
    }
    set(v: FrontFaceType) {
        if (v === this.current && !this.dirty) return;
        this.gl.frontFace(v);
        this.current = v;
        this.dirty = false;
    }
}

export class ProgramValue extends BaseValue<WebGLProgram> {
    getDefault(): WebGLProgram {
        return null;
    }
    set(v?: WebGLProgram | null) {
        if (v === this.current && !this.dirty) return;
        this.gl.useProgram(v);
        this.current = v;
        this.dirty = false;
    }
}

export class ActiveTextureUnit extends BaseValue<TextureUnitType> {
    getDefault(): TextureUnitType {
        return this.gl.TEXTURE0;
    }
    set(v: TextureUnitType) {
        if (v === this.current && !this.dirty) return;
        this.gl.activeTexture(v);
        this.current = v;
        this.dirty = false;
    }
}

export class Viewport extends BaseValue<ViewportType> {
    getDefault(): ViewportType {
        const gl = this.gl;
        return [0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight];
    }
    set(v: ViewportType) {
        const c = this.current;
        if (v[0] === c[0] && v[1] === c[1] && v[2] === c[2] && v[3] === c[3] && !this.dirty) return;
        this.gl.viewport(v[0], v[1], v[2], v[3]);
        this.current = v;
        this.dirty = false;
    }
}

export class BindFramebuffer extends BaseValue<WebGLFramebuffer> {
    getDefault(): WebGLFramebuffer {
        return null;
    }
    set(v?: WebGLFramebuffer | null) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.bindFramebuffer(gl.FRAMEBUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}

export class BindRenderbuffer extends BaseValue<WebGLRenderbuffer> {
    getDefault(): WebGLRenderbuffer {
        return null;
    }
    set(v?: WebGLRenderbuffer | null) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.bindRenderbuffer(gl.RENDERBUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}

export class BindTexture extends BaseValue<WebGLTexture> {
    getDefault(): WebGLTexture {
        return null;
    }
    set(v?: WebGLTexture | null) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.bindTexture(gl.TEXTURE_2D, v);
        this.current = v;
        this.dirty = false;
    }
}

export class BindVertexBuffer extends BaseValue<WebGLBuffer> {
    getDefault(): WebGLBuffer {
        return null;
    }
    set(v?: WebGLBuffer | null) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.bindBuffer(gl.ARRAY_BUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}

export class BindElementBuffer extends BaseValue<WebGLBuffer> {
    getDefault(): WebGLBuffer {
        return null;
    }
    set(v?: WebGLBuffer | null) {
        // Always rebind
        const gl = this.gl;
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}

export class BindVertexArray extends BaseValue<WebGLVertexArrayObject | null> {
    getDefault(): WebGLVertexArrayObject | null {
        return null;
    }
    set(v: WebGLVertexArrayObject | null) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;

        if (isWebGL2(gl)) {
            gl.bindVertexArray(v);
        } else {
            gl.getExtension('OES_vertex_array_object')?.bindVertexArrayOES(v);
        }

        this.current = v;
        this.dirty = false;
    }
}

export class PixelStoreUnpack extends BaseValue<number> {
    getDefault(): number {
        return 4;
    }
    set(v: number) {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, v);
        this.current = v;
        this.dirty = false;
    }
}

export class PixelStoreUnpackPremultiplyAlpha extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean): void {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, ((v as any)));
        this.current = v;
        this.dirty = false;
    }
}

export class PixelStoreUnpackFlipY extends BaseValue<boolean> {
    getDefault(): boolean {
        return false;
    }
    set(v: boolean): void {
        if (v === this.current && !this.dirty) return;
        const gl = this.gl;
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, ((v as any)));
        this.current = v;
        this.dirty = false;
    }
}

class FramebufferAttachment<T> extends BaseValue<T> {
    parent: WebGLFramebuffer;
    context: Context;

    constructor(context: Context, parent: WebGLFramebuffer) {
        super(context);
        this.context = context;
        this.parent = parent;
    }
    getDefault() {
        return null;
    }
}

export class ColorAttachment extends FramebufferAttachment<WebGLTexture> {
    setDirty() {
        this.dirty = true;
    }
    set(v?: WebGLTexture | null): void {
        if (v === this.current && !this.dirty) return;
        this.context.bindFramebuffer.set(this.parent);
        // note: it's possible to attach a renderbuffer to the color
        // attachment point, but thus far MBGL only uses textures for color
        const gl = this.gl;
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, v, 0);

        this.current = v;
        this.dirty = false;
    }
}

export class DepthAttachment extends FramebufferAttachment<WebGLRenderbuffer> {
    set(v?: WebGLRenderbuffer | null): void {
        if (v === this.current && !this.dirty) return;
        this.context.bindFramebuffer.set(this.parent);
        // note: it's possible to attach a texture to the depth attachment
        // point, but thus far MBGL only uses renderbuffers for depth
        const gl = this.gl;
        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}

export class DepthStencilAttachment extends FramebufferAttachment<WebGLRenderbuffer> {
    set(v?: WebGLRenderbuffer | null): void {
        if (v === this.current && !this.dirty) return;
        this.context.bindFramebuffer.set(this.parent);
        // note: it's possible to attach a texture to the depth attachment
        // point, but thus far MBGL only uses renderbuffers for depth
        const gl = this.gl;
        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, v);
        this.current = v;
        this.dirty = false;
    }
}
