/*
 * 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.v2.cfg;

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

import com.mchange.v3.hocon.HoconPropertiesConfigSource;

import static com.mchange.v2.cfg.DelayedLogItem.*;

final class BasicMultiPropertiesConfig extends MultiPropertiesConfig
{
    private final static String HOCON_CFG_CNAME = "com.typesafe.config.Config";
    private final static int    HOCON_PFX_LEN   = 6; // includes colon, hocon:

    final static BasicMultiPropertiesConfig EMPTY = new BasicMultiPropertiesConfig();

    static final class SystemPropertiesConfigSource implements PropertiesConfigSource
    {
	public Parse propertiesFromSource( String identifier ) throws FileNotFoundException, Exception
	{
	    if ( "/".equals( identifier ) )
		return new Parse( (Properties) System.getProperties().clone(), Collections.<DelayedLogItem>emptyList() );
	    else
		throw new Exception(  String.format("Unexpected identifier for System properties: '%s'", identifier) );
	}
    }

    static boolean isHoconPath( String identifier )
    { return (identifier.length() > HOCON_PFX_LEN && identifier.substring(0,6).toLowerCase().equals("hocon:")); }

    private static PropertiesConfigSource configSource( String identifier ) throws Exception
    {
	boolean hocon = isHoconPath( identifier );

	if (!hocon && ! identifier.startsWith("/"))
	    throw new IllegalArgumentException(String.format("Resource identifier '%s' is neither an absolute resource path nor a HOCON path. (Resource paths should be specified beginning with '/' or 'hocon:/')", identifier));

	if ( hocon )
	    {
		try 
		    {
			Class.forName( HOCON_CFG_CNAME );
			return new HoconPropertiesConfigSource();
		    }
		catch (ClassNotFoundException e)
		    {
			//Okay. Apparently the HOCON bridge lib is not available. Let's see if the resource is present.
			int sfx_index = identifier.lastIndexOf('#');
			String resourcePath = sfx_index > 0 ? identifier.substring( HOCON_PFX_LEN, sfx_index ) : identifier.substring( HOCON_PFX_LEN );
			if (BasicMultiPropertiesConfig.class.getResource( resourcePath ) == null)
			    throw new FileNotFoundException( String.format("HOCON lib (typesafe-config) is not available. Also, no resource available at '%s' for HOCON identifier '%s'.", resourcePath, identifier) );
			else
			    throw new Exception(String.format("Could not decode HOCON resource '%s', even though the resource exists, because HOCON lib (typesafe-config) is not available.", identifier), e);
		    }
	    }
	else if ( "/".equals(identifier) )
	    return new SystemPropertiesConfigSource();
	else
	    return new BasicPropertiesConfigSource();
    }

    String[] rps;
    Map  propsByResourcePaths;
    Map  propsByPrefixes;

    List parseMessages;

    Properties propsByKey;

    public BasicMultiPropertiesConfig(String[] resourcePaths)
    { this( resourcePaths, null ); }

    BasicMultiPropertiesConfig(String[] resourcePaths, List delayedLogItems)
    {
	firstInit( resourcePaths, delayedLogItems );
	finishInit( delayedLogItems );
    }

    /*
    public BasicMultiPropertiesConfig(String[] resourcePaths, MLogger logger)
    {
	List delayedLogItems = new LinkedList();

	firstInit( resourcePaths, delayedLogItems );

	if ( logger != null )
	    for ( Iterator ii = delayedLogItems.iterator(); ii.hasNext(); )
	    {
		DelayedLogItem item = (DelayedLogItem) ii.next();
		logger.log( item.getLevel(), item.getText(), item.getException() );
	    }

	finishInit();
    }
    */

    public BasicMultiPropertiesConfig( String notionalResourcePath, Properties props )
    { this( new String[] { notionalResourcePath }, resourcePathToPropertiesMap( notionalResourcePath, props ), Collections.emptyList() ); }

    private static Map resourcePathToPropertiesMap( String notionalResourcePath, Properties props )
    {
	Map out = new HashMap();
	out.put( notionalResourcePath, props );
	return out;
    }

    BasicMultiPropertiesConfig(String[] rps, Map propsByResourcePaths, List parseMessages)
    {
	this.rps                  = rps;
	this.propsByResourcePaths = propsByResourcePaths;

	List dlis = new ArrayList();
	dlis.addAll( parseMessages );
	finishInit( dlis );

	this.parseMessages = dlis;
    }

    // EMPTY
    private BasicMultiPropertiesConfig()
    {
	this.rps = new String[0];
	Map propsByResourcePaths = Collections.emptyMap();
	Map propsByPrefixes = Collections.emptyMap();
	
	List parseMessages = Collections.emptyList();
	
	Properties propsByKey = new Properties();
    }

    private void firstInit( String[] resourcePaths, List delayedLogItems )
    {
	boolean syserr = false;
	if (delayedLogItems == null)
	    {
		delayedLogItems = new ArrayList();
		syserr = true;
	    }

	Map  pbrp = new HashMap();
	List goodPaths = new ArrayList();

	for( int i = 0, len = resourcePaths.length; i < len; ++i )
	    {
		String rp = resourcePaths[i];

		try
		{
		    PropertiesConfigSource cs = configSource( rp );
		    PropertiesConfigSource.Parse parse = cs.propertiesFromSource( rp );
		    pbrp.put( rp, parse.getProperties() );
		    goodPaths.add( rp );
		    delayedLogItems.addAll( parse.getDelayedLogItems() );
		}
		catch ( FileNotFoundException fnfe )
		{ delayedLogItems.add( new DelayedLogItem( Level.FINE, String.format("The configuration file for resource identifier '%s' could not be found. Skipping.", rp) ) ); }
		catch ( Exception e )
		    { delayedLogItems.add( new DelayedLogItem( Level.WARNING, String.format("An Exception occurred while trying to read configuration data at resource identifier '%s'.", rp), e) ); }
	    }
	
	this.rps = (String[]) goodPaths.toArray( new String[ goodPaths.size() ] );
	this.propsByResourcePaths = Collections.unmodifiableMap( pbrp );
	this.parseMessages = Collections.unmodifiableList( delayedLogItems );

	if ( syserr )
	    dumpToSysErr( delayedLogItems );
    }

    /**
     *  rps, propsByResourcePaths, and parseMessages should be set before finishInit()
     */
    private void finishInit( List delayedLogItems )
    {
	boolean syserr = false;
	if (delayedLogItems == null)
	    {
		delayedLogItems = new ArrayList();
		syserr = true;
	    }

	this.propsByPrefixes = Collections.unmodifiableMap( extractPrefixMapFromRsrcPathMap(rps, propsByResourcePaths, delayedLogItems ) );
	this.propsByKey = extractPropsByKey(rps, propsByResourcePaths, delayedLogItems );

	if ( syserr )
	    dumpToSysErr( delayedLogItems );
    }

    public List getDelayedLogItems()
    { return parseMessages; }

    private static void dumpToSysErr( List delayedLogMessages )
    {
	for (Object o : delayedLogMessages)
	    System.err.println( o );
    }

    private static String extractPrefix( String s )
    {
	int lastdot = s.lastIndexOf('.');
	if ( lastdot < 0 )
	{
	    if ( "".equals( s ) )
		return null;
	    else
		return "";
        }
	else
	    return s.substring(0, lastdot);
    }

    private static Properties findProps(String rp, Map pbrp)
    {
	//System.err.println("findProps( " + rp + ", ... )");
	Properties p;
	
	// MOVED THIS LOGIC INTO CONSTRUCTOR ABOVE, TO TREAT SYSTEM PROPS UNIFORMLY
	// WITH THE REST, AND TO AVOID UNINTENTIONAL ATTEMPTS TO READ RESOURCE "/"
	// AS STREAM -- swaldman, 2006-01-19
	
// 	if ( "/".equals( rp ) )
// 	    {
// 		try { p = System.getProperties(); }
// 		catch ( SecurityException e )
// 		    {
// 			System.err.println(BasicMultiPropertiesConfig.class.getName() +
// 					   " Read of system Properties blocked -- ignoring any configuration via System properties, and using Empty Properties! " +
// 					   "(But any configuration via a resource properties files is still okay!)"); 
// 			p = new Properties(); 
// 		    }
// 	    }
// 	else
	p = (Properties) pbrp.get( rp );
	
// 	System.err.println( p );

	return p;
    }

    private static Properties extractPropsByKey( String[] resourcePaths, Map pbrp, List delayedLogItems )
    {
	Properties out = new Properties();
	for (int i = 0, len = resourcePaths.length; i < len; ++i)
	    {
		String rp = resourcePaths[i];
		Properties p = findProps( rp, pbrp );
		if (p == null)
		    {
			delayedLogItems.add( new DelayedLogItem( Level.WARNING, BasicMultiPropertiesConfig.class.getName() + ".extractPropsByKey(): Could not find loaded properties for resource path: " + rp) );
			//System.err.println("Could not find loaded properties for resource path: " + rp);
			continue;
		    }
		for (Iterator ii = p.keySet().iterator(); ii.hasNext(); )
		    {
			Object kObj = ii.next();
			if (!(kObj instanceof String))
			    {
				String message = 
				    BasicMultiPropertiesConfig.class.getName() + ": " +
				    "Properties object found at resource path " +
				    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
				    "' contains a key that is not a String: " +
				    kObj +
				    "; Skipping...";

				/*
				// note that we can not use the MLog library here, because initialization
				// of that library depends on this function.
				System.err.println( BasicMultiPropertiesConfig.class.getName() + ": " +
						    "Properties object found at resource path " +
						    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
						    "' contains a key that is not a String: " +
						    kObj);
				System.err.println("Skipping...");
				*/
				delayedLogItems.add( new DelayedLogItem( Level.WARNING, message) );
				continue;
			    }
			Object vObj = p.get( kObj );
			if (vObj != null && !(vObj instanceof String))
			    {
				String message =
				    BasicMultiPropertiesConfig.class.getName() + ": " +
				    "Properties object found at resource path " +
				    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
				    " contains a value that is not a String: " +
				    vObj +
				    "; Skipping...";

				/*
				// note that we can not use the MLog library here, because initialization
				// of that library depends on this function.
				System.err.println( BasicMultiPropertiesConfig.class.getName() + ": " +
						    "Properties object found at resource path " +
						    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
						    " contains a value that is not a String: " +
						    vObj);
				System.err.println("Skipping...");
				*/
				delayedLogItems.add( new DelayedLogItem( Level.WARNING, message) );
				continue;
			    }

			String key = (String) kObj;
			String val = (String) vObj;
			out.put( key, val );
		    }
	    }
	return out;
    }

    private static Map extractPrefixMapFromRsrcPathMap(String[] resourcePaths, Map pbrp, List delayedLogItems )
    {
	Map out = new HashMap();
	//for( Iterator ii = pbrp.values().iterator(); ii.hasNext(); )
	for (int i = 0, len = resourcePaths.length; i < len; ++i)
	    {
		String rp = resourcePaths[i];
		Properties p = findProps( rp, pbrp );
		if (p == null)
		    {
			String message = BasicMultiPropertiesConfig.class.getName() + ".extractPrefixMapFromRsrcPathMap(): Could not find loaded properties for resource path: " + rp;
			//System.err.println(BasicMultiPropertiesConfig.class.getName() + " -- Could not find loaded properties for resource path: " + rp);
			delayedLogItems.add( new DelayedLogItem( Level.WARNING, message) );
			continue;
		    }
		for (Iterator jj = p.keySet().iterator(); jj.hasNext(); )
		    {
			Object kObj = jj.next();
			if (! (kObj instanceof String))
			    {
				String message =
				    BasicMultiPropertiesConfig.class.getName() + ": " +
				    "Properties object found at resource path " +
				    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
				    "' contains a key that is not a String: " +
				    kObj +
				    "; Skipping...";

				/*
				// note that we can not use the MLog library here, because initialization
				// of that library depends on this function.
				System.err.println( BasicMultiPropertiesConfig.class.getName() + ": " +
						    "Properties object found at resource path " +
						    ("/".equals(rp) ? "[system properties]" : "'" + rp + "'") +
						    "' contains a key that is not a String: " +
						    kObj);
				System.err.println("Skipping...");
				*/

				delayedLogItems.add( new DelayedLogItem( Level.WARNING, message) );
				continue;
			    }

			String key = (String) kObj;
			String prefix = extractPrefix( key );
			while (prefix != null)
			    {
				Properties byPfx = (Properties) out.get( prefix );
				if (byPfx == null)
				    {
					byPfx = new Properties();
					out.put( prefix, byPfx );
				    }
				byPfx.put( key, p.get( key ) );

				prefix=extractPrefix( prefix );
			    }
		    }
	    }
	return out;
    }

    public String[] getPropertiesResourcePaths()
    { return (String[]) rps.clone(); }

    public Properties getPropertiesByResourcePath(String path)
    { 
	Properties out = ((Properties) propsByResourcePaths.get( path )); 
	return (out == null ? new Properties() : out);
    }

    public Properties getPropertiesByPrefix(String pfx)
    {
	Properties out = ((Properties) propsByPrefixes.get( pfx ));
	return (out == null ? new Properties() : out);
    }

    public String getProperty( String key )
    { return propsByKey.getProperty( key ); }

//    public Properties getProperties()
//    { return (Properties) propsByKey.clone(); }

    //TODO: Make this much prettier
    public String dump()
    { return String.format("[ propertiesByResourcePaths -> %s, propertiesByPrefixes -> %s ]", propsByResourcePaths, propsByPrefixes); }

    public String toString()
    { return super.toString() + " " + this.dump(); }
}
