import { math } from '../../core/math/math.js';
import { TEXTURELOCK_NONE, PIXELFORMAT_RGBA8, isCompressedPixelFormat, isIntegerPixelFormat, FILTER_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, ADDRESS_REPEAT, FUNC_LESS, TEXTURETYPE_DEFAULT, TEXTUREPROJECTION_NONE, TEXTUREPROJECTION_CUBE, TEXPROPERTY_MIN_FILTER, TEXPROPERTY_MAG_FILTER, TEXPROPERTY_ADDRESS_U, TEXPROPERTY_ADDRESS_V, TEXPROPERTY_ADDRESS_W, TEXPROPERTY_COMPARE_ON_READ, TEXPROPERTY_COMPARE_FUNC, TEXPROPERTY_ANISOTROPY, isSrgbPixelFormat, pixelFormatLinearToGamma, pixelFormatGammaToLinear, TEXTURETYPE_RGBP, TEXTURETYPE_RGBE, TEXTURETYPE_RGBM, requiresManualGamma, TEXPROPERTY_ALL, TEXTURELOCK_WRITE, getPixelFormatArrayType } from './constants.js';
import { TextureUtils } from './texture-utils.js';

let id = 0;
class Texture {
		constructor(graphicsDevice, options = {}){
				this._gpuSize = 0;
				this.id = id++;
				this._invalid = false;
				this._lockedLevel = -1;
				this._lockedMode = TEXTURELOCK_NONE;
				this.renderVersionDirty = 0;
				this._storage = false;
				this._numLevels = 0;
				this.device = graphicsDevice;
				this.name = options.name ?? '';
				this._width = Math.floor(options.width ?? 4);
				this._height = Math.floor(options.height ?? 4);
				this._format = options.format ?? PIXELFORMAT_RGBA8;
				this._compressed = isCompressedPixelFormat(this._format);
				this._integerFormat = isIntegerPixelFormat(this._format);
				if (this._integerFormat) {
						options.minFilter = FILTER_NEAREST;
						options.magFilter = FILTER_NEAREST;
				}
				this._volume = options.volume ?? false;
				this._depth = Math.floor(options.depth ?? 1);
				this._arrayLength = Math.floor(options.arrayLength ?? 0);
				this._storage = options.storage ?? false;
				this._cubemap = options.cubemap ?? false;
				this._flipY = options.flipY ?? false;
				this._premultiplyAlpha = options.premultiplyAlpha ?? false;
				this._mipmaps = options.mipmaps ?? true;
				this._numLevelsRequested = options.numLevels;
				if (options.numLevels !== undefined) {
						this._numLevels = options.numLevels;
				}
				this._updateNumLevel();
				this._minFilter = options.minFilter ?? FILTER_LINEAR_MIPMAP_LINEAR;
				this._magFilter = options.magFilter ?? FILTER_LINEAR;
				this._anisotropy = options.anisotropy ?? 1;
				this._addressU = options.addressU ?? ADDRESS_REPEAT;
				this._addressV = options.addressV ?? ADDRESS_REPEAT;
				this._addressW = options.addressW ?? ADDRESS_REPEAT;
				this._compareOnRead = options.compareOnRead ?? false;
				this._compareFunc = options.compareFunc ?? FUNC_LESS;
				this._type = options.type ?? TEXTURETYPE_DEFAULT;
				this.projection = TEXTUREPROJECTION_NONE;
				if (this._cubemap) {
						this.projection = TEXTUREPROJECTION_CUBE;
				} else if (options.projection && options.projection !== TEXTUREPROJECTION_CUBE) {
						this.projection = options.projection;
				}
				this._levels = options.levels;
				const upload = !!options.levels;
				if (!this._levels) {
						this._clearLevels();
				}
				this.recreateImpl(upload);
				graphicsDevice.textures.push(this);
		}
		destroy() {
				const device = this.device;
				if (device) {
						const idx = device.textures.indexOf(this);
						if (idx !== -1) {
								device.textures.splice(idx, 1);
						}
						device.scope.removeValue(this);
						this.impl.destroy(device);
						this.adjustVramSizeTracking(device._vram, -this._gpuSize);
						this._levels = null;
						this.device = null;
				}
		}
		recreateImpl(upload = true) {
				const { device } = this;
				this.impl?.destroy(device);
				this.impl = null;
				this.impl = device.createTextureImpl(this);
				this.dirtyAll();
				if (upload) {
						this.upload();
				}
		}
		_clearLevels() {
				this._levels = this._cubemap ? [
						[
								null,
								null,
								null,
								null,
								null,
								null
						]
				] : [
						null
				];
		}
		resize(width, height, depth = 1) {
				const device = this.device;
				this.adjustVramSizeTracking(device._vram, -this._gpuSize);
				this.impl.destroy(device);
				this._clearLevels();
				this._width = Math.floor(width);
				this._height = Math.floor(height);
				this._depth = Math.floor(depth);
				this._updateNumLevel();
				this.impl = device.createTextureImpl(this);
				this.dirtyAll();
		}
		loseContext() {
				this.impl.loseContext();
				this.dirtyAll();
		}
		adjustVramSizeTracking(vram, size) {
				vram.tex += size;
		}
		propertyChanged(flag) {
				this.impl.propertyChanged(flag);
				this.renderVersionDirty = this.device.renderVersion;
		}
		_updateNumLevel() {
				const maxLevels = this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height) : 1;
				const requestedLevels = this._numLevelsRequested;
				this._numLevels = Math.min(requestedLevels ?? maxLevels, maxLevels);
				this._mipmaps = this._numLevels > 1;
		}
		get lockedMode() {
				return this._lockedMode;
		}
		set minFilter(v) {
				if (this._minFilter !== v) {
						if (isIntegerPixelFormat(this._format)) ; else {
								this._minFilter = v;
								this.propertyChanged(TEXPROPERTY_MIN_FILTER);
						}
				}
		}
		get minFilter() {
				return this._minFilter;
		}
		set magFilter(v) {
				if (this._magFilter !== v) {
						if (isIntegerPixelFormat(this._format)) ; else {
								this._magFilter = v;
								this.propertyChanged(TEXPROPERTY_MAG_FILTER);
						}
				}
		}
		get magFilter() {
				return this._magFilter;
		}
		set addressU(v) {
				if (this._addressU !== v) {
						this._addressU = v;
						this.propertyChanged(TEXPROPERTY_ADDRESS_U);
				}
		}
		get addressU() {
				return this._addressU;
		}
		set addressV(v) {
				if (this._addressV !== v) {
						this._addressV = v;
						this.propertyChanged(TEXPROPERTY_ADDRESS_V);
				}
		}
		get addressV() {
				return this._addressV;
		}
		set addressW(addressW) {
				if (!this._volume) {
						return;
				}
				if (addressW !== this._addressW) {
						this._addressW = addressW;
						this.propertyChanged(TEXPROPERTY_ADDRESS_W);
				}
		}
		get addressW() {
				return this._addressW;
		}
		set compareOnRead(v) {
				if (this._compareOnRead !== v) {
						this._compareOnRead = v;
						this.propertyChanged(TEXPROPERTY_COMPARE_ON_READ);
				}
		}
		get compareOnRead() {
				return this._compareOnRead;
		}
		set compareFunc(v) {
				if (this._compareFunc !== v) {
						this._compareFunc = v;
						this.propertyChanged(TEXPROPERTY_COMPARE_FUNC);
				}
		}
		get compareFunc() {
				return this._compareFunc;
		}
		set anisotropy(v) {
				if (this._anisotropy !== v) {
						this._anisotropy = v;
						this.propertyChanged(TEXPROPERTY_ANISOTROPY);
				}
		}
		get anisotropy() {
				return this._anisotropy;
		}
		set mipmaps(v) {
				if (this._mipmaps !== v) {
						if (this.device.isWebGPU) ; else if (isIntegerPixelFormat(this._format)) ; else {
								this._mipmaps = v;
						}
						if (v) this._needsMipmapsUpload = true;
				}
		}
		get mipmaps() {
				return this._mipmaps;
		}
		get numLevels() {
				return this._numLevels;
		}
		get storage() {
				return this._storage;
		}
		get width() {
				return this._width;
		}
		get height() {
				return this._height;
		}
		get depth() {
				return this._depth;
		}
		get format() {
				return this._format;
		}
		get cubemap() {
				return this._cubemap;
		}
		get gpuSize() {
				const mips = this.pot && this._mipmaps && !(this._compressed && this._levels.length === 1);
				return TextureUtils.calcGpuSize(this._width, this._height, this._depth, this._format, mips, this._cubemap);
		}
		get array() {
				return this._arrayLength > 0;
		}
		get arrayLength() {
				return this._arrayLength;
		}
		get volume() {
				return this._volume;
		}
		set type(value) {
				if (this._type !== value) {
						this._type = value;
						this.device._shadersDirty = true;
				}
		}
		get type() {
				return this._type;
		}
		set srgb(value) {
				const currentSrgb = isSrgbPixelFormat(this.format);
				if (value !== currentSrgb) {
						if (value) {
								const srgbFormat = pixelFormatLinearToGamma(this.format);
								if (this._format !== srgbFormat) {
										this._format = srgbFormat;
										this.recreateImpl();
										this.device._shadersDirty = true;
								}
						} else {
								const linearFormat = pixelFormatGammaToLinear(this.format);
								if (this._format !== linearFormat) {
										this._format = linearFormat;
										this.recreateImpl();
										this.device._shadersDirty = true;
								}
						}
				}
		}
		get srgb() {
				return isSrgbPixelFormat(this.format);
		}
		set flipY(flipY) {
				if (this._flipY !== flipY) {
						this._flipY = flipY;
						this._needsUpload = true;
				}
		}
		get flipY() {
				return this._flipY;
		}
		set premultiplyAlpha(premultiplyAlpha) {
				if (this._premultiplyAlpha !== premultiplyAlpha) {
						this._premultiplyAlpha = premultiplyAlpha;
						this._needsUpload = true;
				}
		}
		get premultiplyAlpha() {
				return this._premultiplyAlpha;
		}
		get pot() {
				return math.powerOfTwo(this._width) && math.powerOfTwo(this._height);
		}
		get encoding() {
				switch(this.type){
						case TEXTURETYPE_RGBM:
								return 'rgbm';
						case TEXTURETYPE_RGBE:
								return 'rgbe';
						case TEXTURETYPE_RGBP:
								return 'rgbp';
				}
				return requiresManualGamma(this.format) ? 'srgb' : 'linear';
		}
		dirtyAll() {
				this._levelsUpdated = this._cubemap ? [
						[
								true,
								true,
								true,
								true,
								true,
								true
						]
				] : [
						true
				];
				this._needsUpload = true;
				this._needsMipmapsUpload = this._mipmaps;
				this._mipmapsUploaded = false;
				this.propertyChanged(TEXPROPERTY_ALL);
		}
		lock(options = {}) {
				options.level ??= 0;
				options.face ??= 0;
				options.mode ??= TEXTURELOCK_WRITE;
				this._lockedMode = options.mode;
				this._lockedLevel = options.level;
				const levels = this.cubemap ? this._levels[options.face] : this._levels;
				if (levels[options.level] === null) {
						const width = Math.max(1, this._width >> options.level);
						const height = Math.max(1, this._height >> options.level);
						const depth = Math.max(1, this._depth >> options.level);
						const data = new ArrayBuffer(TextureUtils.calcLevelGpuSize(width, height, depth, this._format));
						levels[options.level] = new (getPixelFormatArrayType(this._format))(data);
				}
				return levels[options.level];
		}
		setSource(source, mipLevel = 0) {
				let invalid = false;
				let width, height;
				if (this._cubemap) {
						if (source[0]) {
								width = source[0].width || 0;
								height = source[0].height || 0;
								for(let i = 0; i < 6; i++){
										const face = source[i];
										if (!face || face.width !== width || face.height !== height || !this.device._isBrowserInterface(face)) {
												invalid = true;
												break;
										}
								}
						} else {
								invalid = true;
						}
						if (!invalid) {
								for(let i = 0; i < 6; i++){
										if (this._levels[mipLevel][i] !== source[i]) {
												this._levelsUpdated[mipLevel][i] = true;
										}
								}
						}
				} else {
						if (!this.device._isBrowserInterface(source)) {
								invalid = true;
						}
						if (!invalid) {
								if (source !== this._levels[mipLevel]) {
										this._levelsUpdated[mipLevel] = true;
								}
								if (source instanceof HTMLVideoElement) {
										width = source.videoWidth;
										height = source.videoHeight;
								} else {
										width = source.width;
										height = source.height;
								}
						}
				}
				if (invalid) {
						this._width = 4;
						this._height = 4;
						if (this._cubemap) {
								for(let i = 0; i < 6; i++){
										this._levels[mipLevel][i] = null;
										this._levelsUpdated[mipLevel][i] = true;
								}
						} else {
								this._levels[mipLevel] = null;
								this._levelsUpdated[mipLevel] = true;
						}
				} else {
						if (mipLevel === 0) {
								this._width = width;
								this._height = height;
						}
						this._levels[mipLevel] = source;
				}
				if (this._invalid !== invalid || !invalid) {
						this._invalid = invalid;
						this.upload();
				}
		}
		getSource(mipLevel = 0) {
				return this._levels[mipLevel];
		}
		unlock() {
				if (this._lockedMode === TEXTURELOCK_NONE) ;
				if (this._lockedMode === TEXTURELOCK_WRITE) {
						this.upload();
				}
				this._lockedLevel = -1;
				this._lockedMode = TEXTURELOCK_NONE;
		}
		upload() {
				this._needsUpload = true;
				this._needsMipmapsUpload = this._mipmaps;
				this.impl.uploadImmediate?.(this.device, this);
		}
		read(x, y, width, height, options = {}) {
				return this.impl.read?.(x, y, width, height, options);
		}
		write(x, y, width, height, data) {
				return this.impl.write?.(x, y, width, height, data);
		}
}

export { Texture };
