import { type Database as DB } from 'better-sqlite3';
import { type Statement } from 'better-sqlite3';
import Database from 'better-sqlite3';
// import sharp for images
import sharp, { FormatEnum, AvailableFormatInfo, Sharp, Metadata, TextAlign } from 'sharp';
import * as fs from 'fs';
import * as https from 'https';

// Map file extensions to sharp formats
const imageFormatMap: { [key: string]: keyof FormatEnum | AvailableFormatInfo } = {
    jpg: 'jpeg',
    jpeg: 'jpeg',
    png: 'png',
    webp: 'webp',
    tiff: 'tiff',
    gif: 'gif'
};

// no need for third party library
const mimeTypeMap: { [key: string]: string } = {
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    png: 'image/png',
    webp: 'image/webp',
    tiff: 'image/tiff',
    gif: 'image/gif'
};

interface HitResult {
    hits: number;
}

interface RGBColor {
    r: number;
    g: number;
    b: number;
}

export interface Type<T> extends Function { new (...args: any[]): T; }


interface HitCounterRenderer {
    render(): Promise<any>;
}

function fileExt(file: string): string {    
    const fileExt = file.split('.').pop();
    if (!fileExt) {
        throw new Error('Invalid file extension');
    }
    return fileExt.toLowerCase();
}


function extToFormat(ext: string): keyof FormatEnum | AvailableFormatInfo {
    const format = imageFormatMap[ext];
    if (!format) {
        throw new Error('Unsupported file extension');
    }
    return format;
}

/**
 * Determine whether the given `path` is a http(s) URL. This method
 * accepts a string value and a URL instance.
 * 
 * inspiration:
 * Marcus Pöhls
 * https://futurestud.io/tutorials/node-js-check-if-a-path-is-a-file-url
 *
 * @param {String|URL} path
 *
 * @returns {Boolean}
 */
function isWebURI(path) {  
    // `path` is neither a string nor a URL -> early return
    if (typeof path !== 'string' && !(path instanceof URL)) {
        return false
    }
  
    try {
        const url = new URL(path)
  
        return url.protocol === 'http:' || url.protocol === 'https:'
    } catch (error) {
        return false
    }
}

class BaseHitCounterRenderer implements HitCounterRenderer {
    protected counter: HitCounter;
    protected config: HitCounterConfig;

    constructor(counter: HitCounter, config: HitCounterConfig) {
        this.counter = counter;
        this.config = config;
    }

    async render(): Promise<any> {
        // render the counter
        throw new Error('Not implemented');
    }

}


class ImageHitCounterRenderer extends BaseHitCounterRenderer {
    private imageType: string;
    private imageFormat: keyof FormatEnum | AvailableFormatInfo;
    private baseImage: Buffer;
    private baseImageMetadata: Metadata;
    constructor(counter: HitCounter, config: HitCounterConfig) {
        super(counter, config);
        this.imageType = fileExt(this.config.imageFile);
        this.imageFormat = extToFormat(this.imageType);
    }

    private async downloadImage(): Promise<Buffer> {
        return new Promise((resolve, reject) => {
            let buf = Buffer.alloc(0);
            const request = https.get(this.config.backgroundImageUrl, (response) => {
                response.on('data', (chunk) => {
                    buf = Buffer.concat([buf, chunk]);
                });
                response.on('end', () => {
                    resolve(buf);
                });
            });
            request.on('error', (err) => {
                fs.unlink(this.config.backgroundCacheFile, () => {
                    reject(err);
                });
            });
        });
    }

    private async cachedBackgroundImage(overWrite: boolean = false): Promise<Sharp> {
        // cache the image
        const dest = this.config.backgroundCacheFile;
        if (fs.existsSync(dest)) {
            if (overWrite) {
                fs.unlinkSync(dest);
            } else {
                return sharp(dest);
            }
        }
        
        // store the image in the cache
        const ext = fileExt(this.config.backgroundImageUrl);
        
        const format = extToFormat(ext);
        if (isWebURI(this.config.backgroundImageUrl)) {
            const buf = await this.downloadImage();
            const image = sharp(buf).toFormat(format);
            await image.toFile(dest);
            return image;
        } else {
            if (fs.existsSync(this.config.backgroundImageUrl)) {
                const image = sharp(this.config.backgroundImageUrl).toFormat(format);
                await image.toFile(dest);
                return image;
            } else {
                throw new Error('Background image does not exist');
            }
        }
    }

    async prepareBaseImage(): Promise<void> {
        if (typeof this.baseImage !== 'undefined') {
            return;
        }
        const bgImage = await this.cachedBackgroundImage();
        const bgInfo = await bgImage.metadata();
        const imgWidth = bgInfo.width;
        const imgHeight = bgInfo.height;
        const imgDensity = bgInfo.density;
        if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined' || typeof imgDensity === 'undefined') {
            throw new Error('Invalid image dimensions');
        }

        let svgFrame = '';
        if (this.config.drawFrame) {
            svgFrame = `<rect x="0" y="0" width="100%" height="100%" fill="none" stroke-alignment="inner" stroke="rgb(${this.config.frameColorRGB.r},${this.config.frameColorRGB.g},${this.config.frameColorRGB.b})" stroke-width="${this.config.borderWidth}" />`;
            
        }
        
        const svgOverlay = `
            <svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">
                <text y="${this.config.textPositionY}" font-family="${this.config.fontFace}" font-size="${this.config.fontSize}">
                    <tspan x="${this.config.textPositionX}" dy="0" fill="rgb(${this.config.textColorRGB.r},${this.config.textColorRGB.g},${this.config.textColorRGB.b})">${this.config.customText}</tspan>
                    <tspan x="${this.config.textPositionX}" dy="1.2em" fill="rgb(${this.config.secondaryTextColorRGB.r},${this.config.secondaryTextColorRGB.g},${this.config.secondaryTextColorRGB.b})">${this.config.secondaryText}</tspan>
                </text>
                ${svgFrame}
            </svg>
        `;

        const overlay = Buffer.from(svgOverlay);

        const composite = bgImage.composite([
            {
                input: overlay,
                top: 0,
                left: 0
            }
        ]);

        const preparedImg = composite.toFormat(this.imageFormat).withMetadata();
        this.baseImageMetadata = await preparedImg.metadata();
        this.baseImage = await preparedImg.toBuffer();

    }

    async render(): Promise<Buffer> {
        const hits = await this.counter.getHits();
        
        const numberX = this.config.numberPositionX;
        const numberY = this.config.numberPositionY;

        await this.prepareBaseImage();

        const imgWidth = this.baseImageMetadata.width;
        const imgHeight = this.baseImageMetadata.height;
        if (typeof imgWidth === 'undefined' || typeof imgHeight === 'undefined') {
            throw new Error('Invalid image dimensions');
        }
        
        const svgNumber = `
            <svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">
                <text x="${numberX}" y="${numberY}" font-family="${this.config.fontFace}" font-size="${this.config.fontSize}" text-anchor="end" fill="rgb(${this.config.numberColorRGB.r},${this.config.numberColorRGB.g},${this.config.numberColorRGB.b})">${hits}</text>
            </svg>
        `;

        const numberOverlay = Buffer.from(svgNumber);

        const composite = sharp(this.baseImage).composite([
            {
                input: numberOverlay,
                top: 0,
                left: 0
            }
        ]);

        return composite.toBuffer();
        
    }
}


interface HitCounterConfig {
    siteId: number;
    // path to the database file
    counterDB: string;
    // Background image URL for the counter display
    backgroundImageUrl: string;
    // local image path
    imageFile: string;
    backgroundCacheFile: string;
    // Text settings to be displayed on the image
    customText: string; // Main headline text
    secondaryText: string; // Secondary descriptive text
    textPositionX: number; // X position of the text
    textPositionY: number; // Y position of the text
    numberPositionX: number; // X position of the number
    numberPositionY: number; // Y position of the number
    fontSize: number; // Font size
    fontFace: string; // Font face
    borderWidth: number; // Border width
    textColorRGB: RGBColor; // Text color
    secondaryTextColorRGB: RGBColor; // Secondary text color
    numberColorRGB: RGBColor; // Number color
    frameColorRGB: RGBColor; // Frame color
    drawFrame: boolean; // Whether to draw a frame around the counter
    renderer: Type<BaseHitCounterRenderer>; // Renderer class for the counter
}



// default configuration
const defaultHitCounterConfig: HitCounterConfig = {
    siteId: 1,
    counterDB: './hits.db',
    backgroundImageUrl: 'https://datakra.sh/assets/example_counter.jpg',
    imageFile: './example_counter.png',
    backgroundCacheFile: './example_counter_bg.png',
    customText: 'Hit Counter!',
    secondaryText: 'Super cyber, super cool!',
    textPositionX: 5,
    textPositionY: 11,
    numberPositionX: 170,
    numberPositionY: 18,
    fontSize: 10,
    fontFace: 'fixed',
    borderWidth: 3,
    textColorRGB: { r: 253, g: 252, b: 1 },
    secondaryTextColorRGB: { r: 0, g: 255, b: 0 },
    numberColorRGB: { r: 255, g: 255, b: 255 },
    frameColorRGB: { r: 255, g: 0, b: 0 },
    drawFrame: true,
    renderer: ImageHitCounterRenderer
};

class HitCounter {
    private db: DB;
    private renderer: BaseHitCounterRenderer;
    private config: HitCounterConfig;
    private preparedGet: Statement;
    private preparedIncrement: Statement;
    private preparedReset: Statement;
    private mimeType: string;

    constructor(renderer: Type<BaseHitCounterRenderer>, config?: HitCounterConfig) {
        this.config = { ...defaultHitCounterConfig, ...config };
        this.renderer = new renderer(this, this.config);
        this.openDB();
        this.setupDB();
        this.setupStatements();
        this.mimeType = mimeTypeMap[fileExt(this.config.imageFile)];
    }

    private openDB(): void {
        const db = new Database(this.config.counterDB, {fileMustExist: false});
        db.pragma('locking_mode = EXCLUSIVE');
        db.pragma('journal_mode = WAL');
        db.pragma('synchronous = OFF');
        this.db = db;
    }

    private setupDB(): void {
        this.db.exec(`
            CREATE TABLE IF NOT EXISTS hit_counter (
                id INTEGER PRIMARY KEY,
                hits INTEGER NOT NULL
            );
        `);
        this.db.exec(`
            INSERT OR IGNORE INTO hit_counter (id, hits)
            VALUES (?, ${this.config.siteId})
        `);
    }

    private closeDB(): void {
        this.db.close();
    }

    private setupStatements(): void {
        this.preparedGet = this.db.prepare(`
            SELECT hits
            FROM hit_counter
            WHERE id = ?
        `);
        this.preparedIncrement = this.db.prepare(`
            UPDATE hit_counter
            SET hits = hits + 1
            WHERE id = ?
        `);
        this.preparedReset = this.db.prepare(`
            UPDATE hit_counter
            SET hits = 0
            WHERE id = ?
        `);
    }

    async getHits(): Promise<number> {
        const result = this.preparedGet.get(this.config.siteId) as HitResult;
        return result.hits;
    }

    async increment(): Promise<void> {
        this.preparedIncrement.run(this.config.siteId);
    }

    async reset(): Promise<void> {
        this.preparedReset.run(this.config.siteId);
    }

    async render(): Promise<any> {
        return this.renderer.render();
    }

    getMimeType(): string {
        return this.mimeType;
    }

    // destructor
    destroy(): void {
        this.closeDB();
    }
    
}




export {
    HitCounter,
    HitCounterConfig,
    HitCounterRenderer,
    BaseHitCounterRenderer,
    ImageHitCounterRenderer
};

export default HitCounter;