package pl.djvuhtml5.client;

import static pl.djvuhtml5.client.TileRenderer.toZoom;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;

import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.ImageData;
import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.typedarrays.shared.Uint8Array;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.lizardtech.djvu.DjVuInfo;
import com.lizardtech.djvu.GMap;
import com.lizardtech.djvu.GRect;
import com.lizardtech.djvu.text.DjVuText;

import pl.djvuhtml5.client.TileRenderer.TileInfo;

public class DataStore {

	private List<DjVuInfo> pageInfos;
	private List<DjVuText> pageTexts;

	private Map<TileInfo, CanvasElement> tiles = new HashMap<>();
	private final CanvasElement missingTileImage;

	private final List<Consumer<Integer>> pageCountListeners = new ArrayList<>();
	private final List<Consumer<Integer>> textListeners = new ArrayList<>();
	private final List<Consumer<Integer>> infoListeners = new ArrayList<>();
	private final List<Consumer<Integer>> tileListeners = new ArrayList<>();

	private final GRect tempRect = new GRect();
	private final TileInfo tempTI = new TileInfo();
	private final int tileSize;
	private CanvasElement bufferCanvas;
	private ImageData bufferImageData;

	public DataStore() {
		missingTileImage = prepareMissingTileImage();
		this.tileSize = DjvuContext.getTileSize();
		bufferCanvas = createImage(tileSize, tileSize);
	}

	private CanvasElement prepareMissingTileImage() {
		int tileSize = DjvuContext.getTileSize();
		CanvasElement canvas = createImage(tileSize, tileSize);
		Context2d context2d = canvas.getContext2d();
		context2d.setFillStyle("white");
		context2d.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
		Image image = new Image();
		final ImageElement imageElement = image.getElement().cast();
		imageElement.getStyle().setProperty("visibility", "hidden");
		Event.setEventListener(imageElement, event -> {
			if (Event.ONLOAD == event.getTypeInt()) {
				missingTileImage.getContext2d().drawImage(imageElement, 0, 0);
				RootPanel.get().getElement().removeChild(imageElement);
			}
		});
		RootPanel.get().getElement().appendChild(imageElement);
		image.setUrl(getBlankImageUrl());
		return canvas;
	}

	private String getBlankImageUrl() {
		Element element = new Label().getElement().cast();
		element.addClassName("blankImage");
		RootPanel.get().getElement().appendChild(element);
		try {
			String url = Djvu_html5.getComputedStyleProperty(element, "background-image");
			url = url.replaceAll("^url\\(['\"]?(.*)['\"]\\)$", "$1");
			return url;
		} finally {
			RootPanel.get().getElement().removeChild(element);
		}
	}

	public void addPageCountListener(Consumer<Integer> listener) {
		pageCountListeners.add(listener);
	}

	public void addTextListener(Consumer<Integer> listener) {
		textListeners.add(listener);
	}

	public void addInfoListener(Consumer<Integer> listener) {
		infoListeners.add(listener);
	}

	public void addTileListener(Consumer<Integer> listener) {
		tileListeners.add(listener);
	}

	public int getPageCount() {
		return pageInfos != null ? pageInfos.size() : 1;
	}

	public void setPageCount(int pageCount) {
		pageInfos = Arrays.asList(new DjVuInfo[pageCount]);
		pageTexts = Arrays.asList(new DjVuText[pageCount]);
		for (Consumer<Integer> l : pageCountListeners)
			l.accept(pageCount);
	}

	public DjVuInfo getPageInfo(int pageNum) {
		return pageInfos != null ? pageInfos.get(pageNum) : null;
	}

	public void setPageInfo(int pageNum, DjVuInfo info) {
		pageInfos.set(pageNum, info);
		for (Consumer<Integer> l : infoListeners)
			l.accept(pageNum);
	}

	public DjVuText getText(int pageNum) {
		return pageTexts != null ? pageTexts.get(pageNum) : null;
	}

	public void setText(int pageNum, DjVuText text) {
		pageTexts.set(pageNum, text);
		for (Consumer<Integer> l : textListeners)
			l.accept(pageNum);
	}

	public CanvasElement[][] getTileImages(int pageNum, int subsample, GRect range, CanvasElement[][] reuse) {
		CanvasElement[][] result = reuse;
		int w = range.width() + 1, h = range.height() + 1;
		if (reuse == null || reuse.length != h || reuse[0].length != w) {
			result = new CanvasElement[h][w];
		}

		tempTI.page = pageNum;
		tempTI.subsample = subsample;
		for (int y = range.ymin; y <= range.ymax; y++)
			for (int x = range.xmin; x <= range.xmax; x++)
				result[y - range.ymin][x - range.xmin] = getTileImage(tempTI.setXY(x, y));

		return result;
	}

	public void releaseTileImages(List<TileInfo> tiles) {
		for (TileInfo tile : tiles)
			this.tiles.remove(tile);
	}

	private CanvasElement getTileImage(TileInfo tileInfo) {
		CanvasElement tileImage = tiles.get(tileInfo);
		if (tileImage != null)
			return tileImage;

		DjVuInfo pageInfo = getPageInfo(tileInfo.page);
		int tileSize = DjvuContext.getTileSize();

		// fill with rescaled other tiles
		ArrayList<TileInfo> fetched = new ArrayList<>();
		tileInfo.getPageRect(tempRect, tileSize, pageInfo);
		GRect tempRect2 = new GRect();
		for (Entry<TileInfo, CanvasElement> entry : tiles.entrySet()) {
			TileInfo ti = entry.getKey();
			if (ti.page == tileInfo.page) {
				ti.getPageRect(tempRect2, tileSize, pageInfo);
				if (tempRect2.intersect(tempRect2, tempRect))
					fetched.add(ti);
			}
		}
		if (fetched.isEmpty())
			return missingTileImage;

		Collections.sort(fetched, (ti1, ti2) -> ti2.subsample - ti1.subsample);
		tileInfo.getScreenRect(tempRect, tileSize, pageInfo);
		tileImage = createImage(tempRect.width(), tempRect.height());
		tiles.put(new TileInfo(tileInfo), tileImage);
		Context2d context = tileImage.getContext2d();

		tileInfo.getScreenRect(tempRect, tileSize, pageInfo);
		double zoom = toZoom(tileInfo.subsample);
		for (TileInfo ti : fetched) {
			context.save();
			double scale = zoom / toZoom(ti.subsample);
			ti.getScreenRect(tempRect2, tileSize, pageInfo);
			context.translate(-tempRect.xmin, -tempRect.ymin);
			context.scale(scale, scale);
			context.translate(tempRect2.xmin, tempRect2.ymin);
			context.drawImage(tiles.get(ti), 0, 0);
			context.restore();
		}
		return tileImage;
	}

	public void setTile(TileInfo tileInfo, GMap bufferGMap) {
		if (bufferImageData == null || bufferImageData.getWidth() != bufferGMap.getDataWidth()
				|| bufferImageData.getHeight() != bufferGMap.getDataHeight()) {
			bufferImageData = bufferCanvas.getContext2d()
					.createImageData(bufferGMap.getDataWidth(), bufferGMap.getDataHeight());
		}
		Uint8Array imageArray = bufferImageData.getData().cast();
		imageArray.set(bufferGMap.getImageData());
		bufferCanvas.getContext2d().putImageData(bufferImageData, -bufferGMap.getBorder(), 0);

		CanvasElement tile = tiles.get(tileInfo);
		if (tile == null) {
			tile = createImage(bufferGMap.getDataWidth() - bufferGMap.getBorder(), bufferGMap.getDataHeight());
			tiles.put(new TileInfo(tileInfo), tile);
		}
		Context2d c = tile.getContext2d();
		c.setFillStyle("white");
		c.fillRect(0, 0, tileSize, tileSize);
		c.drawImage(bufferCanvas, 0, 0);
		for (Consumer<Integer> listener : tileListeners)
			listener.accept(tileInfo.page);
	}

	public static CanvasElement createImage(int width, int height) {
		Canvas canvas = Canvas.createIfSupported();
		canvas.setWidth(width + "px");
		canvas.setCoordinateSpaceWidth(width);
		canvas.setHeight(height + "px");
		canvas.setCoordinateSpaceHeight(height);
		return canvas.getCanvasElement();
	}
}