/*
 * 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.v3.filecache;

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

import com.mchange.v2.io.*;
import com.mchange.v2.log.*;

import com.mchange.v1.io.InputStreamUtils;
import com.mchange.v1.io.OutputStreamUtils;

public final class FileCache
{
    final static MLogger logger = MLog.getLogger( FileCache.class );

    final File    cacheDir;
    final int     buffer_size;
    final boolean read_only;

    final List<URLFetcher> fetchers;

    private InputStream fetchURL(URL u) throws IOException
    {
	List<IOException> exceptions = null;
	for (URLFetcher fetcher : fetchers)
	    {
		try { return fetcher.openStream( u, logger ); }
		catch (FileNotFoundException e)
		    { throw e; }
		catch (IOException e)
		    {
			if (logger.isLoggable( MLevel.FINE ))
			    logger.log( MLevel.FINE, "URLFetcher " + fetcher + " failed on Exception. Will try next fetcher, if any.", e);
			if (exceptions == null)
			    exceptions = new LinkedList<IOException>();
			exceptions.add(e);
		    }
	    }
	if (logger.isLoggable( MLevel.WARNING ))
	    {
		logger.log( MLevel.WARNING, "All URLFetchers failed on URL " + u);
		for (int i = 0, len = exceptions.size(); i < len; ++i)
		    logger.log(MLevel.WARNING, "URLFetcher Exception #" + (i+1), exceptions.get(i));
	    }

	// hopefully we returned long before we reached here...
	throw new IOException("Failed to fetch URL '" + u + "'.");
    } 


    public FileCache(File cacheDir, int buffer_size, boolean read_only) throws IOException
    { this( cacheDir, buffer_size, read_only, Collections.singletonList( (URLFetcher) URLFetchers.DEFAULT ) ); }

    public FileCache(File cacheDir, int buffer_size, boolean read_only, URLFetcher... fetchers) throws IOException
    { this( cacheDir, buffer_size, read_only, Arrays.asList( fetchers ) ); }

    public FileCache(File cacheDir, int buffer_size, boolean read_only, List<URLFetcher> fetchers) throws IOException
    {
	this.cacheDir    = cacheDir;
	this.buffer_size = buffer_size;
	this.read_only   = read_only;

	this.fetchers = Collections.unmodifiableList( fetchers );

	if (cacheDir.exists())
	    {
		if (! cacheDir.isDirectory())
		    loggedIOException(MLevel.SEVERE, cacheDir + "exists and is not a directory. Can't use as cacheDir.");
		else if (! cacheDir.canRead() )
		    loggedIOException(MLevel.SEVERE, cacheDir + "must be readable.");
		else if (! cacheDir.canWrite() && ! read_only)
		    loggedIOException(MLevel.SEVERE, cacheDir + "not writable, and not read only.");
	    }
	else if (! cacheDir.mkdir())
	    loggedIOException(MLevel.SEVERE, cacheDir + "does not exist and could not be created.");
	
	//okay... we have a cacheDir

    }

    public void ensureCached(FileCacheKey key, boolean force_reacquire) throws IOException
    {
	File f = file( key );

	if (! read_only )
	    {
		if ( force_reacquire || (!f.exists()) )
		    {
			InputStream  is = null;
			OutputStream os = null;
			
			try
			    {
				if ( logger.isLoggable( MLevel.FINE ) )
				    logger.log( MLevel.FINE, "Caching file for " + key + " to " + f.getAbsolutePath() + "...");
				File dir = f.getParentFile();
				if (! dir.exists()) dir.mkdirs();

				//is = new BufferedInputStream( key.getURL().openStream(), buffer_size );
				is = new BufferedInputStream( fetchURL( key.getURL() ), buffer_size );
				os = new BufferedOutputStream( new FileOutputStream( f ), buffer_size );
				for (int b = is.read(); b >= 0; b = is.read())
				    os.write(b);

				if ( logger.isLoggable( MLevel.INFO ) )
				    logger.log( MLevel.INFO, "Cached file for " + key + ".");
			    }
			catch (IOException e)
			    {
				logger.log( MLevel.WARNING, "An exception occurred while caching file for " + key + ". Deleting questionable cached file.", e);
				f.delete();
				throw e;
			    }
			finally
			    {
				InputStreamUtils.attemptClose( is );
				OutputStreamUtils.attemptClose( os );
			    }
		    }
		else
		    {
			if ( logger.isLoggable( MLevel.FINE ) )
			    logger.log( MLevel.FINE, "File for " + key + " already exists and force_reacquire is not set.");
		    }
	    }
	else if ( force_reacquire )
	    {
		String message = "force_reacquire canot be set on a read_only FileCache.";
		IllegalArgumentException e = new IllegalArgumentException( message );
		logger.log( MLevel.WARNING, message, e );
		throw e;
	    }
	else if (! f.exists() )
	    {
		String message = "Cache is read only, and file for key '" + key + "' does not exist.";
		FileNotCachedException e = new FileNotCachedException( message );
		logger.log( MLevel.FINE, message, e );
		throw e;
	    }
    }

    public InputStream fetch(FileCacheKey key, boolean force_reacquire) throws IOException
    {
	ensureCached( key, force_reacquire);
	return new FileInputStream( file(key) );
    }

    public boolean isCached(FileCacheKey key) throws IOException
    { return file( key ).exists(); }

    final static FileFilter NOT_DIR_FF = new FileFilter()
    {
	public boolean accept(File f)
	{ return ! f.isDirectory(); }
	};

    static class NotDirAndFileFilter implements FileFilter
    {
	FileFilter ff;

	NotDirAndFileFilter(FileFilter ff)
	{ this.ff = ff; }

	public boolean accept(File f)
	{ return (! f.isDirectory()) && ff.accept(f); }
    }


    public int countCached() throws IOException
    {
	int count = 0;
	for( FileIterator fi = DirectoryDescentUtils.depthFirstEagerDescent( cacheDir, NOT_DIR_FF, false ); fi.hasNext(); )
	    {
		fi.next();
		++count;
	    }
	return count;
    }

    public int countCached(FileFilter filter) throws IOException
    {
	int count = 0;
	for( FileIterator fi = DirectoryDescentUtils.depthFirstEagerDescent( cacheDir, new NotDirAndFileFilter( filter ), false ); fi.hasNext(); )
	    {
		fi.next();
		++count;
	    }
	return count;
    }

    public File fileForKey(FileCacheKey key)
    { return file(key); }

    private File file(FileCacheKey key)
    { return new File( cacheDir, key.getCacheFilePath() ); }

    private void loggedIOException(MLevel lvl, String msg) throws IOException
    {
	IOException e = new IOException( msg );
	logger.log(lvl, msg, e);
	throw e;
    }
}
