/*
 * Distributed as part of mchange-commons-java 0.2.11
 *
 * Copyright (C) 2015 Machinery For Change, Inc.
 *
 * Author: Steve Waldman <swaldman@mchange.com>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of EITHER:
 *
 *     1) The GNU Lesser General Public License (LGPL), version 2.1, as 
 *        published by the Free Software Foundation
 *
 * OR
 *
 *     2) The Eclipse Public License (EPL), version 1.0
 *
 * You may choose which license to accept if you wish to redistribute
 * or modify this work. You may offer derivatives of this work
 * under the license you have chosen, or you may provide the same
 * choice of license which you have been offered here.
 *
 * This software 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.
 *
 * You should have received copies of both LGPL v2.1 and EPL v1.0
 * along with this software; see the files LICENSE-EPL and LICENSE-LGPL.
 * If not, the text of these licenses are currently available at
 *
 * LGPL v2.1: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 *  EPL v1.0: http://www.eclipse.org/org/documents/epl-v10.php 
 * 
 */

package com.mchange.util.impl;

import java.io.*;
import java.util.*;
import com.mchange.io.InputStreamUtils;
import com.mchange.io.OutputStreamUtils;

public class SyncedProperties 
{
  private final static String[] SA_TEMPLATE  = new String[0];
  private final static byte     H_START_BYTE = (byte) '#';
  private final static byte[]   H_LF_BYTES;
  private final static String   ASCII = "8859_1";
  
  static
    {
      try
	{H_LF_BYTES    = System.getProperty("line.separator", "\r\n").getBytes(ASCII);}
      catch (UnsupportedEncodingException e)
	{throw new InternalError("Encoding " + ASCII + " not supported ?!?");}
    }

  Properties props;
  byte[]     headerBytes;
  File       file;
  long       last_mod = -1;

  public SyncedProperties(File file, String header) throws IOException
    {this(file, makeHeaderBytes(header));}

  public SyncedProperties(File file, String[] header) throws IOException
    {this(file, makeHeaderBytes(header));}

  public SyncedProperties(File file) throws IOException
    {this(file, (byte[]) null);}

  private SyncedProperties(File file, byte[] headerBytes) throws IOException
    {
      if (file.exists())
	{
	  if (!file.isFile())
	    throw new IOException(file.getPath() + ": Properties file can't be a directory or special file!");
	  if (headerBytes == null)
	    {
	      //I'd rather not use a localized reader; I'd rather have a fixed, location
	      //independent file format, but Properties uses a localized print stream to write.
	      BufferedReader br = null;
	      try
		{
		  br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));

		  List list = new LinkedList();
		  String line = br.readLine();
		  while (line.trim().equals(""))
		    line = br.readLine();
		  while (line.charAt(0) == '#')
		    list.add(line.substring(1).trim());
		  headerBytes = makeHeaderBytes((String[]) list.toArray(SA_TEMPLATE));
		}
	      finally
		{if (br != null) br.close();}
	    }
	}
      /*
      else //workaround fact that canWrite() returns false one non-existent but creatable files by creating...
	{
	  OutputStream os = null;
	  try
	    {
	      os = new FileOutputStream(file);
	      os.write('\n');
	      os.flush();
	    }
	  finally
	    {if (os == null) os.close();}
	}
      */
      if (!file.canWrite())
	throw new IOException("Can't write to file " + file.getPath());
      this.props       = new Properties();
      this.headerBytes = headerBytes;
      this.file        = file;
      ensureUpToDate();
    }

  public synchronized String getProperty(String property) throws IOException
    {
      ensureUpToDate();
      return props.getProperty(property);
    }

  public synchronized String getProperty(String property, String defaultValue) throws IOException
    {
      String out = props.getProperty(property);
      return (out == null ? defaultValue : out);
    }

  public synchronized void put(String property, String value) throws IOException
    {
      ensureUpToDate();
      props.put(property, value);
      rewritePropsFile();
    }

  public synchronized void remove(String property) throws IOException
    {
      ensureUpToDate();
      props.remove(property);
      rewritePropsFile();
    }

  public synchronized void clear() throws IOException
    {
      ensureUpToDate();
      props.clear();
      rewritePropsFile();
    }

  public synchronized boolean contains(String value) throws IOException
    {
      ensureUpToDate();
      return props.contains(value);
    }

  public synchronized boolean containsKey(String key) throws IOException
    {
      ensureUpToDate();
      return props.containsKey(key);
    }

  public synchronized Enumeration elements() throws IOException
    {
      ensureUpToDate();
      return props.elements();
    }

  public synchronized Enumeration keys() throws IOException
    {
      ensureUpToDate();
      return props.keys();
    }

  public synchronized int size() throws IOException
    {
      ensureUpToDate();
      return props.size();
    }

  public synchronized boolean isEmpty() throws IOException
    {
      ensureUpToDate();
      return props.isEmpty();
    }

  private synchronized void ensureUpToDate() throws IOException
    {
      long new_mod = file.lastModified();
      if (new_mod > last_mod)
	{
	  InputStream is = null; 
	  try
	    {
	      is = new BufferedInputStream(new FileInputStream(file));
	      props.clear();
	      props.load(is);
	      this.last_mod = new_mod;
	    }
	  finally
	    {InputStreamUtils.attemptClose(is);}
	}
    }

  private synchronized void rewritePropsFile() throws IOException
    {
      OutputStream os = null;
      try
	{
	  os = new BufferedOutputStream(new FileOutputStream(file));
	  if (headerBytes != null) os.write(headerBytes);
	  props.store(os, null);
	  os.flush();
	  this.last_mod = file.lastModified();
	}
      finally
	{OutputStreamUtils.attemptClose(os);}
    }

  private static byte[] makeHeaderBytes(String[] header)
    {
      try
	{
	  ByteArrayOutputStream baos = new ByteArrayOutputStream();
	  for (int i = 0, len = header.length; i < len; ++i)
	    {
	      baos.write(H_START_BYTE);
	      baos.write(header[i].getBytes());
	      baos.write(H_LF_BYTES);
	    }
	  return baos.toByteArray();
	}
      catch (IOException e)
	{throw new InternalError("IOException working with ByteArrayOutputStream?!?");}
    }

  private static byte[] makeHeaderBytes(String header)
    {
      try
	{
	  ByteArrayOutputStream baos = new ByteArrayOutputStream();
	  baos.write(H_START_BYTE);
	  baos.write(header.getBytes());
	  baos.write(H_LF_BYTES);
	  return baos.toByteArray();
	}
      catch (IOException e)
	{throw new InternalError("IOException working with ByteArrayOutputStream?!?");}
    }
}
