/*
 * Copyright (C) 2011.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */
package uk.me.parabola.imgfmt.app.typ;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.app.BitWriter;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Writeable;

/**
 * Holds colour information for elements in the typ file.
 *
 * The Colour information can relate to a bitmap or solid shapes.
 *
 * @author Steve Ratcliffe
 */
public class ColourInfo implements Writeable, AlphaAdder {
	private static final int S_NIGHT = 1;
	private static final int S_DAY_TRANSPARENT = 0x2;
	private static final int S_NIGHT_TRANSPARENT = 0x4;
	private static final int S_HAS_BITMAP = 0x8;

	private int numberOfColours;
	private int numberOfSolidColours;

	private boolean hasBitmap;
	private boolean hasBorder;
	private final List<RgbWithTag> colours = new ArrayList<>();
	private final Map<String, Integer> indexMap = new HashMap<>();

	private char width;
	private char height;
	private char charsPerPixel;

	private boolean simple = true;
	private char colourMode;

	/**
	 * Add a colour for this element.
	 * @param tag The xpm tag that represents the colour.
	 * @param rgb The actual colour.
	 */
	public void addColour(String tag, Rgb rgb) {
		RgbWithTag cwt = new RgbWithTag(tag, rgb);
		colours.add(cwt);
	}

	/**
	 * Add a transparent colour. Convenience routine.
	 */
	public void addTransparent(String colourTag) {
		addColour(colourTag, new Rgb(0, 0, 0, 0));
	}

	public void setHasBitmap(boolean hasBitmap) {
		this.hasBitmap = hasBitmap;
	}

	/**
	 * The colour scheme in use. This is a bitmask that has the following bits:
	 * 0 - Has night colour
	 * 1 - day background colour is transparent
	 * 2 - night background colour is transparent
	 * 3 - has bitmap
	 *
	 * If there is no night colour, then set the night background colour bit to be the same as
	 * the day one.
	 *
	 * @return The colour scheme bitmask. The term colour scheme is historical, it doesn't really
	 * describe it.
	 */
	public int getColourScheme() {
		if (numberOfColours == 0)
			numberOfColours = colours.size();
		
		int scheme = 0;
		if (hasBitmap)
			scheme |= S_HAS_BITMAP;

		if (numberOfColours == 4)
			scheme |= S_NIGHT;

		if (!hasBitmap && !hasBorder && numberOfColours == 2)
			scheme |= S_NIGHT | S_DAY_TRANSPARENT | S_NIGHT_TRANSPARENT;
		
		if (numberOfColours < 2 || colours.get(1).isTransparent())
			scheme |= S_DAY_TRANSPARENT;
		if (numberOfColours == 4 && (colours.get(3).isTransparent()))
			scheme |= S_NIGHT_TRANSPARENT;

		if ((scheme & S_NIGHT) == 0)
			if ((scheme & S_DAY_TRANSPARENT) != 0)
				scheme |= S_NIGHT_TRANSPARENT;

		return scheme;
	}

	/**
	 * Get the number of bits per pixel that will be used in the written bitmap.
	 *
	 * This depends on the colour mode and number of colours to be represented.
	 */
	public int getBitsPerPixel() {
		if (simple)
			return 1;

		// number of colours includes the transparent pixel in colormode=0x10 so this
		// works for all colour modes.
		int nc = numberOfColours;
		if (nc == 0)
			return 24;
		else if (nc < 2)
			return 1;
		else if (nc < 4)
			return 2;
		else if (nc < 16)
			return 4;
		else
			return 8;
	}

	/**
	 * Write out the colours only.
	 */
	public void write(ImgFileWriter writer) {
		if (colourMode == 0x20) {
			writeColours20(writer);
		} else {
			for (Rgb rgb : colours) {
				if (!rgb.isTransparent())
					rgb.write(writer, (byte) 0x10);
			}
		}
	}

	/**
	 * Write out the colours in the colormode=x20 case.
	 */
	private void writeColours20(ImgFileWriter writer) {
		BitWriter bw = new BitWriter();
		for (Rgb rgb : colours) {
			bw.putn(rgb.getB(), 8);
			bw.putn(rgb.getG(), 8);
			bw.putn(rgb.getR(), 8);

			int alpha = 0xff - rgb.getA();
			alpha = alphaRound4(alpha);

			bw.putn(alpha, 4);
		}
		writer.put(bw.getBytes(), 0, bw.getLength());
	}

	/**
	 * Round alpha value to four bits.
	 * @param alpha The original alpha value eg 0xf0.
	 * @return Rounded alpha to four bits eg 0xe.
	 */
	static int alphaRound4(int alpha) {
		int top = (alpha >> 4) & 0xf;
		int low = alpha & 0xf;

		int diff = low-top;
		if (diff > 8)
			top++;
		else if (diff < -8)
			top--;
		return top;
	}

	public int getIndex(String tag) {
		Integer ind = indexMap.get(tag);

		// If this is a simple bitmap (for line or polygon), then the foreground colour is
		// first and so has index 0, but we want the foreground to have index 1, so reverse.
		if (simple)
			ind = ~ind;

		return ind;
	}

	public void setWidth(int width) {
		this.width = (char) width;
	}

	public void setHeight(int height) {
		this.height = (char) height;
	}

	public void setNumberOfColours(int numberOfColours) {
		this.numberOfColours = numberOfColours;
	}

	public void setCharsPerPixel(int charsPerPixel) {
		this.charsPerPixel = (char) (charsPerPixel == 0 ? 1 : charsPerPixel);
	}

	public int getNumberOfColours() {
		return numberOfColours;
	}

	public int getNumberOfSColoursForCM() {
		if (colourMode == 0x10)
			return numberOfSolidColours;
		else
			return numberOfColours;
	}

	public int getCharsPerPixel() {
		return charsPerPixel;
	}

	public int getHeight() {
		return height;
	}

	public int getWidth() {
		return width;
	}

	public int getColourMode() {
		return colourMode;
	}

	public void setColourMode(int colourMode) {
		this.colourMode = (char) colourMode;
	}

	public void setSimple(boolean simple) {
		this.simple = simple;
	}

	public void setHasBorder(boolean hasBorder) {
		this.hasBorder = hasBorder;
	}

	/**
	 * Replace the last pixel with a pixel with the same colour components and the given
	 * alpha.
	 *
	 * This is used when the alpha value is specified separately to the colour values in the
	 * input file.
	 * @param alpha The alpha value to be added to the pixel. This is a real alpha, not a transparency.
	 */
	public void addAlpha(int alpha) {
		int last = colours.size();
		RgbWithTag rgb = colours.get(last - 1);
		rgb = new RgbWithTag(rgb, alpha);
		colours.set(last - 1, rgb);
	}

	/**
	 * Analyse the colour pallet and normalise it.
	 *
	 * Try to work out what is required from the supplied colour pallet and set the colour mode
	 * and rearrange transparent pixels if necessary to be in the proper place.
	 *
	 * At the end we build the index from colour tag to pixel index.
	 *
	 * @param simple If this is a line or polygon.
	 * @return A string describing the validation failure.
	 */
	public String analyseColours(boolean simple) {
		setSimple(simple);
		
		if (simple) {
			// There can be up to four colours, no partial transparency, and a max of one transparent pixel
			// in each of the day/night sections.

			if (numberOfColours > 4)
				return "Too many colours for a line or polygon";
			if (numberOfColours == 0)
				return "Line or polygon cannot have zero colours";

			// Putting the transparent pixel first is common, so reverse if found
			if (colours.get(0).isTransparent()) {
				if (numberOfColours < 2)
					return "Only colour cannot be transparent for line or polygon";
				swapColour(0, 1);
			}
			if (numberOfColours > 2 && colours.get(2).isTransparent()) {
				if (numberOfColours < 4)
					return "Only colour cannot be transparent for line or polygon";
				swapColour(2, 3);
			}

			// There can only be one transparent pixel per colour pair
			if (numberOfColours > 1 && colours.get(0).isTransparent())
				return "Both day foreground and background are transparent";
			if (numberOfColours > 3 && colours.get(2).isTransparent())
				return "Both night foreground and background are transparent";

		} else {
			int transIndex = 0; // index of last transparent pixel, only used when there is only one
			int nTrans = 0; // completely transparent
			int nAlpha = 0; // partially transparent
			int count = 0;  // total number of colours
			for (RgbWithTag rgb : colours) {
				if (rgb.isTransparent()) {
					nTrans++;
					transIndex = count;
				}

				if (rgb.getA() != 0xff && rgb.getA() != 0)
					nAlpha++;
				count++;
			}

			if (nAlpha > 0 || (count > 0 && count == nTrans)) {
				// If there is any partial transparency we need colour mode 0x20
				// Also if there is only one pixel and it is transparent, since otherwise there would be zero
				// solid colours and that is a special case used to indicate a true colour pixmap.
				colourMode = 0x20;

			} else if (nTrans == 1) {
				colourMode = 0x10;

				// Ensure the transparent pixel is at the end
				RgbWithTag rgb = colours.remove(transIndex);
				colours.add(rgb);
			} 
		}

		int count = 0;
		for (RgbWithTag rgb : colours) {
			indexMap.put(rgb.getTag(), count++);
			if (!rgb.isTransparent())
				numberOfSolidColours++;
		}

		return null;
	}

	private void swapColour(int c1, int c2) {
		RgbWithTag tmp = colours.get(c1);
		colours.set(c1, colours.get(c2));
		colours.set(c2, tmp);
	}
}
