/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package flex2.compiler.io;

import java.io.*;
import java.util.List;

/**
 * A helper class used by classes doing file operations.  Part of it's
 * original purpose was to consolidate J# handling, but J# support was
 * dropped many years ago.
 *
 * @author Clement Wong
 */
public final class FileUtil
{
	public static final boolean caseInsensitive = (System.getProperty("os.name").toLowerCase().startsWith("windows") ||
												   System.getProperty("os.name").toLowerCase().startsWith("mac"));

	/**
	 * Return an instance of File...
	 *
	 * @param path
	 * @return File
	 */
	public static File openFile(String path)
	{
		try
		{
			return new File(path);
		}
		catch (Exception e) // J#.NET throws an error if the file is not found...
		{
			return null;
		}
	}

	public static File openFile(String path, boolean mkdir)
	{
		File f = new File(path).getAbsoluteFile();

		if (mkdir)
		{
			new File(f.getParent()).mkdirs();
		}

		return f;
	}

	/**
	 * Return an instance of File...
	 *
	 * @param parentPath
	 * @param fileName
	 * @return File
	 */
	public static File openFile(File parentPath, String fileName)
	{
		try
		{
			return new File(parentPath, fileName);
		}
		catch (Exception e) // J#.NET throws an error if the file is not found...
		{
			return null;
		}
	}

	/**
	 * Create a temp file. Write the input stream to the file...
	 *
	 * @param in
	 * @return
	 * @throws IOException
	 */
	public static File createTempFile(InputStream in) throws IOException
	{
		File temp = File.createTempFile("Flex2_", "");
		writeBinaryFile(temp, in);
		return temp;
	}

	/**
	 * Return an input stream with BOM consumed...
	 *
	 * @param file
	 * @return
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public static InputStream openStream(File file) throws FileNotFoundException, IOException
	{
		return openStream(file.getAbsolutePath());
	}

	/**
	 * Return an input stream with BOM consumed...
	 *
	 * @param path
	 * @return
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public static InputStream openStream(String path) throws FileNotFoundException, IOException
	{
		return openStream(new FileInputStream(path));
	}

	/**
	 * Return an input stream with BOM consumed...
	 *
	 * @param in
	 * @return
	 * @throws IOException
	 */
	public static InputStream openStream(InputStream in) throws IOException
	{
		byte[] bom = new byte[3];

		in.read(bom, 0, 3);

		if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf)
		{
			return in;
		}
		// else if (bom[0] == 0xff && bom[1] == 0xfe || bom[0] == 0xfe && bom[1] == 0xff)
		else
		{
			return new BOMInputStream(bom, in);
		}
	}

	/**
	 * It's a normal input stream but it serves BOM before bytes in the stream...
	 */
    //TODO make a reader version of this class so calls like
    //         > new BufferedReader(new InputStreamReader(FileUtil.openStream(f)))
    //     don't have so many levels of indirection to get to the stream
	private static final class BOMInputStream extends InputStream
	{
		private BOMInputStream(byte[] bom, InputStream in)
		{
			this.bom = bom;
			bomSize = bom.length;
			this.in = in;
			index = 0;
		}

		private byte[] bom;
		private final int bomSize;
		private final InputStream in;
		private int index;

		public int read() throws IOException
		{
			if (bom == null)
			{
				return in.read();
			}
			else
			{
				int c = bom[index];
				if (index == bomSize - 1)
				{
					bom = null;
				}
				index += 1;
				return c;
			}
		}

		public int read(byte b[], int off, int len) throws IOException
		{
			if (bom == null)
			{
				return in.read(b, off, len);
			}

            int count = 0;
            while ((index < 3) && (count < len))
            {
                b[off+count] = bom[index++];
                ++count;
            }
            if (index == 3)
            {
                bom = null;
            }
            if (len <= count)
                return count;
            int r = in.read(b, off+count, len-count);
            return (r == -1)? -1 : r + count;
		}

		public long skip(long n) throws IOException
		{
			throw new UnsupportedOperationException("supports read() and close() only...");
		}

		public int available() throws IOException
		{
			int num = in.available();
			if (bom == null)
			{
				return num;
			}
			else
			{
				return (bomSize - index) + num;
			}
		}

		public void close() throws IOException
		{
			in.close();
		}

		public synchronized void mark(int readlimit)
		{
			throw new UnsupportedOperationException("supports read() and close() only...");
		}

		public synchronized void reset() throws IOException
		{
			throw new UnsupportedOperationException("supports read() and close() only...");
		}

		public boolean markSupported()
		{
			return false;
		}
	}

	/**
	 * Write a stream of binary data to a file.
	 * @param file
	 * @param in
	 * @throws IOException
	 */
	public static void writeBinaryFile(File file, InputStream in) throws IOException
	{
		OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
		in = new BufferedInputStream(in);
        try
        {
            streamOutput(in, out);
        }
        finally
        {
            out.close();
        }
    }

    public static void streamOutput(InputStream in, OutputStream out)
            throws IOException
    {
        int len = 8192;
        byte[] bytes = new byte[len];
        while ((len = in.read(bytes, 0, len)) != -1)
        {
            out.write(bytes, 0, len);
        }
        // C: caller is responsible for closing the streams.
        out.flush();
    }

    /**
	 * Write a String to a file
	 * @param fileName
	 * @param output
	 * @throws java.io.IOException
	 */
	public static void writeFile(String fileName, String output) throws FileNotFoundException, IOException
	{
		File file = openFile(fileName);
		if (file == null)
		{
			throw new FileNotFoundException(fileName);
		}

		Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF8"));
		try
		{
			out.write(output);
			out.flush();
		}
		finally
		{
			out.close();
		}
	}

	public static void writeBinaryFile(String fileName, byte[] output) throws FileNotFoundException, IOException
	{
		File file = openFile(fileName);
		if (file == null)
		{
			throw new FileNotFoundException(fileName);
		}

		OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
		try
		{
			out.write(output);
			out.flush();
		}
		finally
		{
			out.close();
		}
	}

	public static String getCanonicalPath(String path)
	{
		File f = openFile(path);
		return (f == null) ? null : getCanonicalPath(f);
	}
	
	public static String getCanonicalPath(File f)
	{
		try
		{
			return f.getCanonicalPath();
		}
		catch (IOException ex)
		{
			return f.getAbsolutePath();
		}
	}

	public static File getCanonicalFile(File f)
	{
		try
		{
			return f.getCanonicalFile();
		}
		catch (IOException ex)
		{
			return f.getAbsoluteFile();
		}
	}

	public static String readFile(File f)
	{
		BufferedReader file = null;
		StringBuilder buffer = new StringBuilder((int) f.length());
		String lineSep = System.getProperty("line.separator");

		try
		{
            /* TODO
             * Do we need a BufferedReader layered on a BufferedInputStream?
             * There are so many levels of indirection here that it would be a good idea
             * to have an InputBufferReader (like InputBufferStream) and openReader()
             */
			file = new BufferedReader(new InputStreamReader(FileUtil.openStream(f)));
			String s = null;
			while ((s = file.readLine()) != null)
			{
				buffer.append(s);
				buffer.append(lineSep);
			}
			return buffer.toString();
		}
		catch (FileNotFoundException ex)
		{
			return null;
		}
		catch (IOException ex)
		{
			return null;
		}
		finally
		{
			if (file != null)
			{
				try
				{
					file.close();
				}
				catch (IOException ex)
				{
				}
			}
		}
	}

	public static String readLine(String f, int line)
	{
		BufferedReader file = null;
		try
		{
			file = new BufferedReader(new InputStreamReader(FileUtil.openStream(f), "UTF8"));
			for (int i = 0; i < line - 1 && file.readLine() != null; i++);
			return file.readLine();
		}
		catch (FileNotFoundException ex)
		{
			return null;
		}
		catch (IOException ex)
		{
			return null;
		}
		finally
		{
			if (file != null)
			{
				try
				{
					file.close();
				}
				catch (IOException ex)
				{
				}
			}
		}
	}

	public static byte[] readBytes(String f, int line)
	{
		BufferedInputStream in = null;
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		boolean lastIsCR = false;
		int pos = 0, ch = 0, count = 0;
		try
		{
			in = new BufferedInputStream(FileUtil.openStream(f));
			while ((ch = in.read()) != -1)
			{
				if (lastIsCR)
				{
					count++;
					if (line == count)
					{
						return baos.toByteArray();
					}
					else
					{
						baos.reset();
					}

					if (ch == '\n')
					{
						lastIsCR = false;
					}
					else if (ch == '\r')
					{
						lastIsCR = true;
					}
					else
					{
						baos.write(ch);
						lastIsCR = false;
					}
				}
				else
				{
					if (ch == '\n')
					{
						count++;
						if (line == count)
						{
							return baos.toByteArray();
						}
						else
						{
							baos.reset();
						}
						lastIsCR = false;
					}
					else if (ch == '\r')
					{
						lastIsCR = true;
					}
					else
					{
						baos.write(ch);
						lastIsCR = false;
					}
				}
				pos++;
			}
			return null;
		}
		catch (FileNotFoundException ex)
		{
			return null;
		}
		catch (IOException ex)
		{
			return null;
		}
		finally
		{
			if (in != null) { try { in.close(); } catch (IOException ex) {} }
		}
	}
	
	/**
	 * check whether the provided path is a subdirectory of the list of directories and vice versa.
	 * @param path
	 * @param directories
	 * @return -1 if not
	 */
	public static int isSubdirectoryOf(File pathFile, List directories)
	{
		String path = pathFile.toString();
		for (int j = 0, size = directories.size(); j < size; j++)
		{
			File dirFile = FileUtil.getCanonicalFile((File) directories.get(j));
			String dir = dirFile.toString();
			if (! pathFile.getParent().equals(dirFile.getParent()) &&
			    (path.length() > dir.length() && path.startsWith(dir) ||
			     dir.length() > path.length() && dir.startsWith(path)))
			{
				return j;
			}
		}
		return -1;
	}
	
	/**
	 * check whether the provided directory is a subdirectory of the list of directories.
	 * @param directory
	 * @param directories
	 * @return -1 if not
	 */
	public static int isSubdirectoryOf(String directory, VirtualFile[] directories)
	{
		File pathFile = FileUtil.openFile(directory);
		String path = pathFile.toString();
		for (int j = 0, size = directories == null ? 0 : directories.length; j < size; j++)
		{
			File dirFile = FileUtil.openFile(directories[j].getName());
			String dir = dirFile.toString();
			if (!pathFile.getParent().equals(dirFile.getParent()) && path.length() > dir.length() && path.startsWith(dir))
			{
				return j;
			}
		}
		return -1;
	}
	
	public static String getExceptionMessage(Exception ex)
	{
		return getExceptionMessage(ex, true);
	}

	public static String getExceptionMessage(Exception ex, boolean stackDump)
	{
		String m = ex.getMessage();
		
		if (m == null)
		{
			m = ex.getLocalizedMessage();
		}
		
		if (m == null)
		{
			m = "(" + ex.getClass().getName() + ")";
			if (stackDump)
			{
				StringWriter s = new StringWriter();
				PrintWriter p = new PrintWriter(s, true);
				ex.printStackTrace(p);
				m += s.getBuffer().toString();
			}
		}
		
		return m;
	}
}
