/*
 * 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.
 *  
 */

/*
 * Created on Oct 19, 2004
 */
package org.apache.jmeter.services;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;

import org.apache.jmeter.gui.JMeterFileFilter;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;

/**
 * 
 * The point of this class is to provide thread-safe access to files, and to
 * provide some simplifying assumptions about where to find files and how to
 * name them. For instance, putting supporting files in the same directory as
 * the saved test plan file allows users to refer to the file with just it's
 * name - this FileServer class will find the file without a problem.
 * Eventually, I want all in-test file access to be done through here, with the
 * goal of packaging up entire test plans as a directory structure that can be
 * sent via rmi to remote servers (currently, one must make sure the remote
 * server has all support files in a relative-same location) and to package up
 * test plans to execute on unknown boxes that only have Java installed.
 */
public class FileServer {
	private static final Logger log = LoggingManager.getLoggerForClass();

    private static final String DEFAULT_BASE = JMeterUtils.getProperty("user.dir");
    
	private File base;

    //TODO - make "files" and "random" static as the class is a singleton?
    
	private final Map files = new HashMap();
    
	private static final FileServer server = new FileServer();

	private final Random random = new Random();

	private FileServer() {
		base = new File(DEFAULT_BASE);
		log.info("Default base="+DEFAULT_BASE);
	}

	public static FileServer getFileServer() {
		return server;
	}

    public void resetBase() throws IOException{
        setBasedir(DEFAULT_BASE);
    }
    
	public synchronized void setBasedir(String basedir) throws IOException {
		if (filesOpen()) {
			throw new IOException("Files are still open, cannot change base directory");
		}
		files.clear();
		if (basedir != null) {
			base = new File(basedir);
			if (!base.isDirectory()) {
				base = base.getParentFile();
			}
			log.info("Set new base="+base);
		}
	}

	public synchronized String getBaseDir() {
		return base.getAbsolutePath();
	}

    /**
     * Creates an association between a filename and a File inputOutputObject,
     * and stores it for later use - unless it is already stored.
     * 
     * @param filename - relative (to base) or absolute file name (must not be null)
     */
	public synchronized void reserveFile(String filename) {
		reserveFile(filename,null);
	}

    /**
     * Creates an association between a filename and a File inputOutputObject,
     * and stores it for later use - unless it is already stored.
     * 
     * @param filename - relative (to base) or absolute file name (must not be null)
     * @param charsetName - the character set encoding to use for the file (may be null)
     */
	public synchronized void reserveFile(String filename, String charsetName) {
	    reserveFile(filename, charsetName, filename);
	}

    /**
     * Creates an association between a filename and a File inputOutputObject,
     * and stores it for later use - unless it is already stored.
     * 
     * @param filename - relative (to base) or absolute file name (must not be null)
     * @param charsetName - the character set encoding to use for the file (may be null)
     * @param alias - the name to be used to access the object (must not be null)
     */
    public synchronized void reserveFile(String filename, String charsetName, String alias) {
        if (filename == null){
            throw new IllegalArgumentException("Filename must not be null");
        }
        if (alias == null){
            throw new IllegalArgumentException("Alias must not be null");
        }
        if (!files.containsKey(alias)) {
            File f = new File(filename); 
            FileEntry file = 
                new FileEntry(f.isAbsolute() ? f : new File(base, filename),null,charsetName);
            if (filename.equals(alias)){
                log.info("Stored: "+filename);                
            } else {
                log.info("Stored: "+filename+" Alias: "+alias);
            }
            files.put(alias, file);
        }
    }

   /**
	 * Get the next line of the named file, recycle by default.
	 *
     * @param filename
	 * @return String containing the next line in the file
	 * @throws IOException
	 */
    public String readLine(String filename) throws IOException {
      return readLine(filename, true);
    }

   /**
	 * Get the next line of the named file.
	 * 
     * @param filename
     * @param recycle - should file be restarted at EOF?
	 * @return String containing the next line in the file (null if EOF reached and not recycle)
	 * @throws IOException
	 */
	public synchronized String readLine(String filename, boolean recycle) throws IOException {
		FileEntry fileEntry = (FileEntry) files.get(filename);
		if (fileEntry != null) {
			if (fileEntry.inputOutputObject == null) {
				fileEntry.inputOutputObject = createBufferedReader(fileEntry, filename);
            } else if (!(fileEntry.inputOutputObject instanceof Reader)) {
                throw new IOException("File " + filename + " already in use");
            }
			BufferedReader reader = (BufferedReader) fileEntry.inputOutputObject;
			String line = reader.readLine();
			if (line == null && recycle) {
				reader.close();
				reader = createBufferedReader(fileEntry, filename);
				fileEntry.inputOutputObject = reader;
				line = reader.readLine();
			}
            if (log.isDebugEnabled()) { log.debug("Read:"+line); }
			return line;
		}
		throw new IOException("File never reserved: "+filename);
	}
	
	private BufferedReader createBufferedReader(FileEntry fileEntry, String filename) throws IOException { 
		FileInputStream fis = new FileInputStream(fileEntry.file);				
		InputStreamReader isr = null;
        // If file encoding is specified, read using that encoding, otherwise use default platform encoding
		String charsetName = fileEntry.charSetEncoding;
		if(charsetName != null && charsetName.trim().length() > 0) {
			isr = new InputStreamReader(fis, charsetName); 
		} else {
			isr = new InputStreamReader(fis); 
		}
		return new BufferedReader(isr);
	}

	public synchronized void write(String filename, String value) throws IOException {
		FileEntry fileEntry = (FileEntry) files.get(filename);
		if (fileEntry != null) {
			if (fileEntry.inputOutputObject == null) {
				fileEntry.inputOutputObject = createBufferedWriter(fileEntry, filename);
			} else if (!(fileEntry.inputOutputObject instanceof Writer)) {
				throw new IOException("File " + filename + " already in use");
			}
			BufferedWriter writer = (BufferedWriter) fileEntry.inputOutputObject;
            if (log.isDebugEnabled()) { log.debug("Write:"+value); }
			writer.write(value);
		} else {
            throw new IOException("File never reserved: "+filename);      
        }
	}

	private BufferedWriter createBufferedWriter(FileEntry fileEntry, String filename) throws IOException { 
		FileOutputStream fos = new FileOutputStream(fileEntry.file);				
		OutputStreamWriter osw = null;
        // If file encoding is specified, write using that encoding, otherwise use default platform encoding
		String charsetName = fileEntry.charSetEncoding;
		if(charsetName != null && charsetName.trim().length() > 0) {
			osw = new OutputStreamWriter(fos, charsetName); 
		} else {
			osw = new OutputStreamWriter(fos); 
		}
		return new BufferedWriter(osw);
	}

	public void closeFiles() throws IOException {
		Iterator iter = files.entrySet().iterator();
		while (iter.hasNext()) {
            Map.Entry me = (Map.Entry) iter.next();
			closeFile((String)me.getKey(),(FileEntry)me.getValue() );
		}
		files.clear();
	}

	/**
	 * @param name
	 * @throws IOException
	 */
	public synchronized void closeFile(String name) throws IOException {
		FileEntry fileEntry = (FileEntry) files.get(name);
		closeFile(name, fileEntry);
	}

    private void closeFile(String name, FileEntry fileEntry) throws IOException {
        if (fileEntry != null && fileEntry.inputOutputObject != null) {
            log.info("Close: "+name);
            if (fileEntry.inputOutputObject instanceof Reader) {
                ((Reader) fileEntry.inputOutputObject).close();
            } else if (fileEntry.inputOutputObject instanceof Writer) {
                ((Writer) fileEntry.inputOutputObject).close();
            } else { 
                log.error("Unknown inputOutputObject type : " + fileEntry.inputOutputObject.getClass());
            }
			fileEntry.inputOutputObject = null;
		}
    }

	protected boolean filesOpen() {
		Iterator iter = files.values().iterator();
		while (iter.hasNext()) {
			FileEntry fileEntry = (FileEntry) iter.next();
            if (fileEntry.inputOutputObject != null) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Method will get a random file in a base directory
     * TODO hey, not sure this method belongs here.  FileServer is for threadsafe
     * File access relative to current test's base directory.  
	 * 
	 * @param basedir
	 * @return a random File from the basedir that matches one of the extensions
	 */
	public File getRandomFile(String basedir, String[] extensions) {
		File input = null;
		if (basedir != null) {
			File src = new File(basedir);
			if (src.isDirectory() && src.list() != null) {
				File[] lfiles = src.listFiles(new JMeterFileFilter(extensions));
				int count = lfiles.length;
				input = lfiles[random.nextInt(count)];
			}
		}
		return input;
	}
    
    private static class FileEntry{
        private File file;
        private Object inputOutputObject; // Reader/Writer
        private String charSetEncoding;
        FileEntry(File f, Object o, String e){
            file=f;
            inputOutputObject=o;
            charSetEncoding=e;
        }
    }
}
