/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: ProjectLibrary.java
 * Project management tool: library information
 * Written by: Steven M. Rubin
 *
 * Copyright (c) 2006, Static Free Software. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.project;

import com.sun.electric.database.hierarchy.Cell;
import com.sun.electric.database.hierarchy.Library;
import com.sun.electric.database.hierarchy.View;
import com.sun.electric.database.variable.Variable;
import com.sun.electric.tool.Job;
import com.sun.electric.tool.JobException;
import com.sun.electric.tool.io.FileType;
import com.sun.electric.tool.user.dialogs.OpenFile;
import com.sun.electric.util.TextUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;


/**
 * Class to describe libraries checked into the Project Management system.
 */
public class ProjectLibrary implements Serializable
{
	/** the project directory */				private String                     projDirectory;
	/** Library associated with project file */	private Library                    lib;
	/** all cell records in the project */		private List<ProjectCell>          allCells;
	/** cell records by Cell in the project */	private HashMap<Cell,ProjectCell>  byCell;
	/** I/O channel for project file */			private transient RandomAccessFile raf;
	/** Lock on file when updating it */		private transient FileLock         lock;

//	void validate()
//	{
//		for(ProjectCell pc : allCells)
//		{
//			Cell c = pc.getCell();
//			if (c != null && !c.isLinked()) System.out.println("HEY! "+c+" IS NOT LINKED");
//		}
//		for(Cell c : byCell.keySet())
//		{
//			if (c != null && !c.isLinked()) System.out.println("HEY! "+c+" IS NOT LINKED IN BYCELLS");
//		}
//	}

	private ProjectLibrary()
	{
		allCells = new ArrayList<ProjectCell>();
		byCell = new HashMap<Cell,ProjectCell>();
	}

	static ProjectLibrary createProject(Library lib)
	{
		// create a new project database
		ProjectLibrary pl = new ProjectLibrary();
		pl.lib = lib;

		// figure out the location of the project file
		Variable var = lib.getVar(Project.PROJPATHKEY);
		if (var == null) return pl;
		URL url = TextUtils.makeURLToFile((String)var.getObject());
		if (!TextUtils.URLExists(url))
		{
			url = null;
			if (Project.getRepositoryLocation().length() > 0)
			{
				url = TextUtils.makeURLToFile(Project.getRepositoryLocation() + File.separator + lib.getName() + File.separator + Project.PROJECTFILE);
				if (!TextUtils.URLExists(url)) url = null;
			}
			if (url == null)
			{
				String userFile = OpenFile.chooseInputFile(FileType.PROJECT, "Find Project File for " + lib, null);
				if (userFile == null) return pl;
				url = TextUtils.makeURLToFile(userFile);
			}
		}

		// prepare to read the project file
		String projectFile = url.getFile();
		String projDir = "";
		int sepPos = projectFile.lastIndexOf('/');
		if (sepPos >= 0) projDir = projectFile.substring(0, sepPos);
		try
		{
			pl.raf = new RandomAccessFile(projectFile, "r");
		} catch (FileNotFoundException e)
		{
			System.out.println("Cannot read file: " + projectFile);
			return pl;
		}

		// learn the repository location if this path is valid
		if (Project.getRepositoryLocation().length() == 0)
		{
			String repositoryLocation = null;
			if (sepPos > 1)
			{
				int nextSepPos = projectFile.lastIndexOf('/', sepPos-1);
				if (nextSepPos >= 0) repositoryLocation = projectFile.substring(0, nextSepPos);
			}
			if (repositoryLocation == null)
			{
				Job.getUserInterface().showInformationMessage(
					"You should setup Project Management by choosing a Repository location.  Use the 'Project Management' tab under General Preferences",
					"Setup Project Management");
			} else
			{
				Project.setRepositoryLocation(repositoryLocation);
			}
		}

		pl.projDirectory = projDir;
		pl.loadProjectFile();

		try
		{
			pl.raf.close();
		} catch (IOException e)
		{
			System.out.println("Error closing project file");
		}
		return pl;
	}

	/**
	 * Method to add a ProjectCell to this ProjectLibrary.
	 * Keeps the list sorted.
	 * @param pc the ProjectCell to add.
	 */
	void addProjectCell(ProjectCell pc)
	{
		allCells.add(pc);
		Collections.sort(allCells, new OrderedProjectCells());
	}

	/**
	 * Method to remove a ProjectCell from this ProjectLibrary.
	 * Keeps the list sorted.
	 * @param pc the ProjectCell to remove.
	 */
	void removeProjectCell(ProjectCell pc)
	{
		for(ProjectCell c : allCells)
		{
			if (!c.getCellName().equals(pc.getCellName())) continue;
			if (c.getVersion() != pc.getVersion()) continue;
			if (c.getView() != pc.getView()) continue;
			allCells.remove(c);
			break;
		}
		if (pc.getCell() != null) byCell.remove(pc.getCell());
		Collections.sort(allCells, new OrderedProjectCells());
	}

	void linkProjectCellToCell(ProjectCell pc, Cell cell)
	{
		if (cell == null)
		{
			pc.setLatestVersion(false);
			byCell.remove(pc.getCell());
		} else
		{
			byCell.put(cell, pc);
		}
		pc.setCell(cell);
	}

	void ignoreCell(Cell cell)
	{
		byCell.remove(cell);
	}

	boolean isEmpty() { return allCells.size() == 0; }

	Library getLibrary() { return lib; }

	Iterator<ProjectCell> getProjectCells() { return allCells.iterator(); }

	String getProjectDirectory() { return projDirectory; }

	void setProjectDirectory(String dir) { projDirectory = dir; }

	/**
	 * Class to sort project cells.
	 */
    private static class OrderedProjectCells implements Comparator<ProjectCell>
    {
        public int compare(ProjectCell pc1, ProjectCell pc2)
        {
        	int diff = pc1.getCellName().compareTo(pc2.getCellName());
        	if (diff != 0) return diff;
        	diff = pc1.getView().getFullName().compareTo(pc2.getView().getFullName());
        	if (diff != 0) return diff;
        	return pc1.getVersion() - pc2.getVersion();
        }
    }

	ProjectCell findProjectCell(Cell cell)
	{
		ProjectCell pc = byCell.get(cell);
		return pc;
	}

	ProjectCell findProjectCellByNameView(String name, View view)
	{
		for(ProjectCell pc : allCells)
		{
			if (pc.getCellName().equals(name) && pc.getView() == view) return pc;
		}
		return null;
	}

	ProjectCell findProjectCellByNameViewVersion(String name, View view, int version)
	{
		for(ProjectCell pc : allCells)
		{
			if (pc.getCellName().equals(name) && pc.getView() == view && pc.getVersion() == version) return pc;
		}
		return null;
	}

	/**
	 * Method to lock this project file.
	 * Throws JobException on error.
	 */
	void lockProjectFile()
		throws JobException
	{
		String errMsg = tryLockProjectFile();
		if (errMsg != null)
		{
			throw new JobException(
				"Cannot lock the project file (" + errMsg + ").  It may be in use by another user, or it may be damaged.");
		}
	}

	/**
	 * Method to lock a set of project files.
	 * @param projectFiles the set of project files.
	 * Throws JobException on error.
	 */
	static void lockManyProjectFiles(Set<ProjectLibrary> projectFiles)
		throws JobException
	{
		List<ProjectLibrary> didThem = new ArrayList<ProjectLibrary>();
		for(ProjectLibrary pl : projectFiles)
		{
			String errMsg = pl.tryLockProjectFile();
			if (errMsg != null)
			{
				// failed, unlock those already locked
				for(ProjectLibrary uPl : didThem)
					uPl.releaseProjectFileLock(false);
				throw new JobException(
					"Cannot lock the project file for library " + pl.lib.getName() +
					"(" + errMsg + ").  It may be in use by another user, or it may be damaged.");
			}
			didThem.add(pl);
		}
	}

	/**
	 * Method to lock this project file.
	 * @return error message on error, null on success.
	 */
	private String tryLockProjectFile()
	{
		// prepare to read the project file
		String projectFile = projDirectory + File.separator + Project.PROJECTFILE;
		try
		{
			raf = new RandomAccessFile(projectFile, "rw");
		} catch (FileNotFoundException e)
		{
			return "Cannot read file " + projectFile;
		}

		FileChannel fc = raf.getChannel();
		try
		{
			lock = fc.lock();
		} catch (IOException e1)
		{
			String errMsg = "Unable to lock project file";
			try
			{
				raf.close();
			} catch (IOException e2)
			{
				errMsg = "Unable to close project file";
			}
			raf = null;
			return errMsg;
		}
		String errMsg = loadProjectFile();
		if (errMsg != null)
		{
			try
			{
				lock.release();
				raf.close();
			} catch (IOException e)
			{
				errMsg += "; Unable to release project file lock";
			}
			raf = null;
		}
		return errMsg;
	}

	/**
	 * Method to release the lock on this project file.
	 * @param save true to rewrite it first.
	 */
	void releaseProjectFileLock(boolean save)
	{
		if (save)
		{
			FileChannel fc = raf.getChannel();
			try
			{
				fc.position(0);
				fc.truncate(0);
				for(ProjectCell pc : allCells)
				{
					String line = "::" + pc.getCellName() + ":" + pc.getVersion() + "-" +
						pc.getView().getFullName() + "." + pc.getLibExtension() + ":" +
						pc.getOwner() + ":" + pc.getLastOwner() + ":" + pc.getComment() + "\n";
					ByteBuffer bb = ByteBuffer.wrap(line.getBytes());
					fc.write(bb);
				}
			} catch (IOException e)
			{
				System.out.println("Error saving project file");
			}
		}
		try
		{
			lock.release();
			raf.close();
		} catch (IOException e)
		{
			System.out.println("Unable to unlock and close project file");
			lock = null;
		}
	}
	/**
	 * Method to unlock a set of project files.
	 * @param projectFiles the set of project files.
	 */
	static void releaseManyProjectFiles(Set<ProjectLibrary> projectFiles)
	{
		for(ProjectLibrary pl : projectFiles)
		{
			pl.releaseProjectFileLock(true);
		}
	}

	/**
	 * Method to read the project file into memory.
	 * @return an error message on error (null if OK).
	 */
	private String loadProjectFile()
	{
		allCells.clear();
		byCell.clear();

		// read the project file
		int [] colonPos = new int[6];
		for(;;)
		{
			String userLine = null;
			try
			{
				userLine = raf.readLine();
			} catch (IOException e)
			{
				userLine = null;
			}
			if (userLine == null) break;

			ProjectCell pc = new ProjectCell(null, this);
			int prevPos = 0;
			for(int i=0; i<6; i++)
			{
				colonPos[i] = userLine.indexOf(':', prevPos);
				prevPos = colonPos[i] + 1;
				if (prevPos <= 0)
				{
					return "Too few keywords in project file: " + userLine;
				}
			}
			if (colonPos[0] != 0)
			{
				return "Missing initial ':' in project file: " + userLine;
			}

			// get cell name
			pc.setCellName(userLine.substring(colonPos[1]+1, colonPos[2]));

			// get version
			String section = userLine.substring(colonPos[2]+1, colonPos[3]);
			int dashPos = section.indexOf('-');
			if (dashPos < 0)
			{
				return "Missing '-' after version number in project file: " + userLine;
			}
			int dotPos = section.indexOf('.');
			if (dotPos < 0)
			{
				return "Missing '.' after view type in project file: " + userLine;
			}
			pc.setVersion(TextUtils.atoi(section.substring(0, dashPos)));

			// get view
			String viewPart = section.substring(dashPos+1, dotPos);
			pc.setView(View.findView(viewPart));

			// get file type
			String fileType = section.substring(dotPos+1);
			if (fileType.equals("elib")) pc.setLibType(FileType.ELIB); else
				if (fileType.equals("jelib")) pc.setLibType(FileType.JELIB); else
					if (fileType.equals("txt")) pc.setLibType(FileType.READABLEDUMP); else
			{
				return "Unknown library type in project file: " + userLine;
			}

			// get owner
			pc.setOwner(userLine.substring(colonPos[3]+1, colonPos[4]));

			// get last owner
			pc.setLastOwner(userLine.substring(colonPos[4]+1, colonPos[5]));

			// get comments
			pc.setComment(userLine.substring(colonPos[5]+1));

			// check for duplication
			for(ProjectCell opc : allCells)
			{
				if (opc == pc) continue;
				if (!opc.getCellName().equalsIgnoreCase(pc.getCellName())) continue;
				if (opc.getView() != pc.getView()) continue;
				if (opc.getVersion() != pc.getVersion()) continue;
				System.out.println("Error in project file: version " + pc.getVersion() + ", view '" +
					pc.getView().getFullName() + "' of cell '" + pc.getCellName() + "' exists twice");
			}

			// find the cell associated with this entry
			pc.setLatestVersion(false);
			String cellName = pc.describeWithVersion();
			Cell cell = lib.findNodeProto(cellName);
			pc.setCell(cell);
			if (cell != null)
			{
				if (cell.getVersion() > pc.getVersion())
				{
					if (!pc.getOwner().equals(Project.getCurrentUserName()))
					{
						if (pc.getOwner().length() == 0)
						{
							System.out.println("WARNING: " + cell + " is being edited, but it is not checked-out");
						} else
						{
							System.out.println("WARNING: " + cell + " is being edited, but it is checked-out to " + pc.getOwner());
						}
					}
				}
				byCell.put(cell, pc);
			}

			// link it in
//			allCells.add(pc);
		}

		// determine the most recent views
		HashMap<String,ProjectCell> mostRecent = new HashMap<String,ProjectCell>();
		for(ProjectCell pc : allCells)
		{
			String cellEntry = pc.describe();
			ProjectCell recent = mostRecent.get(cellEntry);
			if (recent != null && recent.getVersion() > pc.getVersion()) continue;
			mostRecent.put(cellEntry, pc);
		}
		for(ProjectCell pc : allCells)
		{
			String cellEntry = pc.describe();
			ProjectCell recent = mostRecent.get(cellEntry);
			pc.setLatestVersion(recent == pc);
		}
		return null;
	}
}
