package net.sourceforge.droid64.d64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**<pre style='font-family:sans-serif;'>
 * Created on 07.07.2017<br>
 *
 *   droiD64 - A graphical filemanager for D64 files<br>
 *   Copyright (C) 2004 Wolfram Heyer<br>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   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.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * @author henrik
 * </pre>
 */
public class Utility {

	public static final String EMPTY = "";
	public static final String SPACE = " ";
	/** PETSCII padding white space character */
	public static final byte BLANK = (byte) 0xa0;

	private static final String ERR_REQUIRED_DATA_MISSING = "Required data is missing. ";
	private static final String ERR_READ_ERROR = "Failed to read from file. ";
	private static final String ERR_ZIP_READ_ERROR = "Failed to read zip file. ";
	private static final String ERR_ZIP_WRITE_ERROR = "Failed to write zip file. ";

	/** Size of buffer reading compressed data */
	private static final int INPUT_BUFFER_SIZE = 65536;
	/** Size of buffer writing uncompressed data to byte[] */
	private static final int OUTPUT_BUFFER_SIZE = 1048576;

	/** PETSCII-ASCII mappings (ASCII to PETSCII mapping. Using 0x20 for invisible characters in PETSCII charset) */
	protected static final int[] PETSCII_TABLE = {
			0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // 00-0f
			0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,	// 10-1f
			0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,	// 20-2f
			0x30, 0x31, 0x32, 0x33,	0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, // 30-3f
			0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,	0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, // 40-4f
			0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,	0x5c, 0x5d, 0x5e, 0xa4, // 50-5f
			0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,	// 60-6f
			0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,	// 70-7f
			0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,	// 80-8f
			0x20, 0x20, 0x20, 0x20,	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,	// 90-9f
			0x20, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, // a0-af
			0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,	// b0-bf
			0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, // c0-cf
			0x70, 0x71, 0x72, 0x73,	0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,	// d0-df
			0x20, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,	// e0-ef
			0xb0, 0xb1, 0xb2, 0xb3,	0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,	// f0-ff
			0x7e
	};
	/** Valid characters when mapping from PC to CBM file names */
	private static final String VALID_PC_CBM_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 []()/;:<>.-_+&%$@#!";

	private Utility() {
	}

	/**
	 * Write data to file on local file system.
	 *
	 * @param targetFile
	 *            the name of the file (without path)
	 * @param data
	 *            the data to write
	 * @throws CbmException
	 *             when error
	 */
	public static void writeFile(File targetFile, byte[] data) throws CbmException {
		if (targetFile == null || data == null) {
			throw new CbmException(ERR_REQUIRED_DATA_MISSING);
		}
		try (FileOutputStream output = new FileOutputStream(targetFile)) {
			output.write(data, 0, data.length);
		} catch (Exception e) {
			throw new CbmException(ERR_READ_ERROR + e.getMessage(), e);
		}
	}

	/**
	 * Read file into byte array
	 *
	 * @param file
	 *            file to read from
	 * @return byte array
	 * @throws CbmException
	 *             when error
	 */
	public static byte[] readFile(File file) throws CbmException {
		byte[] data;
		try (FileInputStream input = new FileInputStream(file);
				ByteArrayOutputStream out = new ByteArrayOutputStream()) {
			byte[] buffer = new byte[65536];
			int read;
			while ((read = input.read(buffer)) != -1) {
				out.write(buffer, 0, read);
			}
			data = out.toByteArray();
		} catch (Exception e) {
			throw new CbmException(ERR_READ_ERROR + e.getMessage(), e);
		}
		return data;
	}

	/**
	 * Read gzipped file.
	 *
	 * @param fileName
	 *            name of zip file
	 * @return byte array with uncompressed data
	 * @throws CbmException
	 *             when failure
	 */
	public static byte[] readGZippedFile(String fileName) throws CbmException {
		try (FileInputStream fis = new FileInputStream(fileName);
				GZIPInputStream gis = new GZIPInputStream(fis, INPUT_BUFFER_SIZE);
				ByteArrayOutputStream bos = new ByteArrayOutputStream(OUTPUT_BUFFER_SIZE)) {
			while (gis.available() == 1) {
				bos.write(gis.read());
			}
			return bos.toByteArray();
		} catch (IOException e) {
			throw new CbmException(ERR_ZIP_READ_ERROR + e.getMessage(), e);
		}
	}

	/**
	 * Write data to a gzipped file
	 *
	 * @param fileName
	 *            name of zip file to create
	 * @param data
	 *            the data
	 * @throws CbmException
	 *             when error
	 */
	public static void writeGZippedFile(String fileName, byte[] data) throws CbmException {
		if (data == null) {
			return;
		}
		try (FileOutputStream output = new FileOutputStream(fileName);
				GZIPOutputStream zipStream = new GZIPOutputStream(output)) {
			zipStream.write(data);
		} catch (IOException e) {
			throw new CbmException(ERR_ZIP_WRITE_ERROR + e.getMessage(), e);
		}
	}

	/**
	 * @param file
	 *            file to check
	 * @return true if file seems to be gzipped.
	 * @throws CbmException
	 *             if error
	 */
	public static boolean isGZipped(String file) throws CbmException {
		try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
			return GZIPInputStream.GZIP_MAGIC == (raf.read() & 0xff | ((raf.read() << 8) & 0xff00));
		} catch (IOException e) {
			throw new CbmException("Failed to open zip header. " + e.getMessage(), e);
		}
	}

	public static String trimTrailing(String str) {
		if (str == null || str.isEmpty()) {
			return str;
		}
		return str.replaceAll("\\s+$", Utility.EMPTY);
	}
	/**
	 * @param data the data
	 * @param pos the index to get value from
	 * @return Get signed byte as int from data[pos]
	 */
	public static int getInt8(byte[] data, int pos) {
		if (data == null || pos >= data.length) {
			return 0;
		} else {
			int n = data[pos] & 0xff;
			return (n & 0x80) != 0 ? n | ~0xff : n;
		}
	}

	/**
	 * Get 16 bit value, with LSB first.
	 * @param data the data
	 * @param pos the index to get value from
	 * @return Get two unsigned bytes as int from data[pos]
	 */
	public static int getInt16(byte[] data, int pos) {
		if (data == null || pos + 1 >= data.length) {
			return 0;
		} else {
			return data[pos] & 0xff | (data[pos + 1] & 0xff) << 8;
		}
	}

	/**
	 * Get 32 bit value with LSB first.
	 * @param data the data
	 * @param pos the index to get value from
	 * @return the 32 bit value
	 */
	public static int getInt32(byte[] data, int pos) {
		return (data[pos] & 0xff) | ((data[pos + 1] & 0xff) << 8) | ((data[pos + 2] & 0xff) << 16) | ((data[pos + 3] & 0xff) << 24);
	}

	public static void setInt16(byte[] data, int pos, int value) {
		data[pos] = (byte) (value & 0xff);
		data[pos + 1] = (byte) ((value >>> 8) & 0xff);
	}

	public static void setInt32(byte[] data, int pos, int value) {
		data[pos] = (byte) (value & 0xff);
		data[pos + 1] = (byte) ((value >>> 8) & 0xff);
		data[pos + 2] = (byte) ((value >>> 16) & 0xff);
		data[pos + 3] = (byte) ((value >>> 24) & 0xff);
	}

	public static String getString(byte[] data, int pos, int length) {
		char[] chars = new char[length];
		for (int i=0; i< length; i++) {
			chars[i] = pos + i < data.length ? (char)( data[pos + i] & 0xff) : 0;
		}
		return new String(chars);
	}

	/**
	 * Set string into bytes, if string is shorter than length, then pad it with 0xA0 (blank)
	 * @param data the byes to insert string into
	 * @param pos the position to start writing at
	 * @param string the string
	 * @param length the length of the string
	 */
	public static void setPaddedString(byte[] data, int pos, String string, int length) {
		for (int i=0; i < length; i++) {
			if (i < string.length()) {
				data[pos + i] = (byte) string.charAt(i);
			} else {
				data[pos + i] = BLANK;
			}
		}
	}

	/**
	 * Copy bytes from short[] to byte[].
	 * @param fromData the short[] to copy from
	 * @param toData the byte[] to copy to
	 * @param fromPos start reading at this position in fromData
	 * @param toPos start writing to this position in toData
	 * @param length the number of bytes to copy
	 */
	public static void copyBytes(short[] fromData, byte[] toData, int fromPos, int toPos, int length) {
		for (int i = 0; i < length; i++) {
			toData[toPos + i] = (byte) (fromData[fromPos + i] & 0xff);
		}
	}

	/**
	 * Copy bytes from byte[] to byte[].
	 * @param fromData the byte[] to copy from
	 * @param toData the byte[] to copy to
	 * @param fromPos start reading at this position in fromData
	 * @param toPos start writing to this position in toData
	 * @param length the number of bytes to copy
	 */
	public static void copyBytes(byte[] fromData, byte[] toData, int fromPos, int toPos, int length) {
		for (int i = 0; i < length; i++) {
			toData[toPos + i] = fromData[fromPos + i];
		}
	}

	public static String getTrimmedString(byte[] data, int pos, int length) throws CbmException {
		byte[] tmp = new byte[length];
		for (int i = 0; i < length; i++) {
			byte b = data[pos+i];
			tmp[i] = b == 0xa0 ? 0x20 : b;
		}
		try {
			return new String(tmp, "ISO-8859-1").trim();
		} catch (UnsupportedEncodingException e) {	// NOSONAR
			throw new CbmException("Unsupported character encoding.", e);
		}
	}

	/**
	 * Convert a PC filename to a proper CBM filename.<BR>
	 * @param orgName orgName
	 * @param maxLength max length
	 * @return the CBM filename
	 */
	public static String cbmFileName(String orgName, int maxLength) {
		char[] fileName = new char[maxLength];
		int out = 0;
		for (int i=0; i<maxLength && i<orgName.length(); i++) {
			char c = Character.toUpperCase(orgName.charAt(i));
			if (VALID_PC_CBM_CHARS.indexOf(c) >= 0) {
				fileName[out++] = c;
			}
		}
		return new String(Arrays.copyOfRange(fileName, 0, out));
	}

	/**
	 * Create string using 7-bit characters.
	 * @param data the data
	 * @param pos position of first byte
	 * @param len number of bytes
	 * @return String
	 */
	public static String getCpmString(byte[] data, int pos, int len) {
		char[] string = new char[len];
		for (int i=0; i<len; i++) {
			string[i] = (char) (data[pos + i] & 0x7f);
		}
		return new String(string);
	}

}