/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.masterfs.filebasedfs.fileobjects;

import java.io.FileNotFoundException;
import java.util.Iterator;
import org.netbeans.modules.masterfs.filebasedfs.utils.FSException;
import org.openide.filesystems.FileLock;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Set;
import org.netbeans.modules.masterfs.filebasedfs.utils.FileInfo;
import org.openide.ErrorManager;
import org.openide.filesystems.FileAlreadyLockedException;
import org.openide.util.NbBundle;


/**
 * This implementation doesn' allow to get two or more FileLocks for
 * one identical file. This means that in editor there is possible to open two
 * editor tabs but there isn't possible to modify them at the same time.
 *
 * This implemenation creates new special locking file. This file exists on disk as long
 * as the FileLock isValid.  This locking files are
 * hidden by FileBasedFileSystem. Moreover this file naming
 * convention corresponds with defaul regular expression for hiding this files by VisibilityQuery
 * (just in case that these files wont be hidden FileBasedFileSystem).<P>
 * <p/>
 * There might happen that these locking files won't be deleted because of
 * crash of JVM, not released lock and so one. But these corrupted locks must
 * be recognized and can't prevent from providing next locks.<P>
 * <p/>
 * This implementation isn't responsible:
 * - for preventing from opening two or more tabs in editor for one file on disk.
 * - for silent refusing of editing already locked file<P>
 *
 * @author Radek Matous
 */


public class WriteLock extends FileLock {
    static final Set allLocks = new HashSet();
    private File lockFile;
    
    static {        
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                for (Iterator it = allLocks.iterator(); it.hasNext();) {
                    String path = (String) it.next();
                    File f = new File(path);
                    if (f.exists() && WriteLockUtils.hasActiveLockFileSigns(path)) {
                        f.delete();
                    }
                    
                }
            }            
        });
    }
    
    static WriteLock tryLock(final File file) throws IOException  {
        return WriteLock.getInstance(file, false);
    }
    
    static WriteLock tryNioLock(final File file) throws IOException  {
        return WriteLock.getInstance(file, true);
    }

    static WriteLock tryLightWeightLock(final File file) throws IOException  {
        return WriteLock.getLightWeightInstance(file);
    }
    
    private static WriteLock getLightWeightInstance(final File file)  throws IOException {
        File lckFile = WriteLockUtils.getAssociatedLockFile(file);
        boolean isAlreadyLocked = WriteLock.allLocks.contains(lckFile.getAbsolutePath());
        if (isAlreadyLocked) {
            throw new FileAlreadyLockedException(NbBundle.getMessage(
                WriteLock.class, "EXC_FileAlreadyLocked", file.getAbsolutePath()));    
        }
        
        return new WriteLock(lckFile);
    }
    
    private static WriteLock getInstance(final File file, boolean nioLock)  throws IOException {
        boolean isCreated = false;
        IOException toFire = null;
        
        File lckFile = WriteLockUtils.getAssociatedLockFile(file);
        if (!lckFile.exists()) {
            isCreated = true;
        }
        RandomAccessFile raf = getAccessToFile(lckFile);//NOI18N
        assert lckFile.exists();
        
        FileChannel channel = raf.getChannel();
        
        
        if (channel != null && channel.isOpen()) {
            try {
                String content = WriteLockUtils.getContentOfLckFile(lckFile, channel);
                if (content == null && (isCreated || lckFile.length() == 0)) {
                    assert lckFile.length() == 0;
                    content = WriteLockUtils.writeContentOfLckFile(lckFile, channel);
                }
                
                if (content != null && !WriteLock.allLocks.contains(content)) {
                    if (channel != null && channel.isOpen() && !nioLock) {
                        channel.close();
                    }
                    return (nioLock) ? new NioLock(lckFile, channel, channel.tryLock()) : new WriteLock(lckFile);
                }
            } catch (IOException iex) {
                toFire =  iex;
            }
        }
        
        if (channel != null && channel.isOpen()) {
            channel.close();
        }
        
        if (isCreated && lckFile.exists()) {
            lckFile.delete();
        }
        
        if (toFire == null) {
            if (lckFile.exists()) {
                toFire = new FileAlreadyLockedException(NbBundle.getMessage(
                    WriteLock.class, "EXC_FileLockAlreadyExists", file.getAbsolutePath(), lckFile.getAbsolutePath()));                    
            } else {
                toFire = new FileAlreadyLockedException(NbBundle.getMessage(
                    WriteLock.class, "EXC_FileAlreadyLocked", file.getAbsolutePath()));    
            }
        }
        
        FSException.annotateException(toFire);
        throw toFire;
    }

    private static RandomAccessFile getAccessToFile(final File f) throws FileNotFoundException {
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(f, "rwd");//NOI18N
        } catch (FileNotFoundException e) {
            FileNotFoundException fex = e;
            if (!f.exists()) {
                fex = (FileNotFoundException)new FileNotFoundException(e.getLocalizedMessage()).initCause(e);
            } else if (!f.canRead()) {
                fex = (FileNotFoundException)new FileNotFoundException(e.getLocalizedMessage()).initCause(e);
            } else if (!f.canWrite()) {
                fex = (FileNotFoundException)new FileNotFoundException(e.getLocalizedMessage()).initCause(e);
            } else if (f.getParentFile() == null) {
                fex = (FileNotFoundException)new FileNotFoundException(e.getLocalizedMessage()).initCause(e);
            } else if (!f.getParentFile().exists()) {
                fex = (FileNotFoundException)new FileNotFoundException(e.getLocalizedMessage()).initCause(e);
            } 
            throw fex;
        }
        return raf;
    }
    
    
    private WriteLock(File lockFile) {
        this.lockFile = lockFile;
        register();
    }
    
    public boolean isValid(final File f) {
        boolean retVal = isValid();
        
        if (retVal) {
            final File associatedLockFile = WriteLockUtils.getAssociatedLockFile(f);
            retVal = lockFile.equals(associatedLockFile);
        } else {
        }
        return retVal;
    }
    
    public void releaseLock() {
        synchronized (WriteLockFactory.class) {
            if (isValid()) {
                deregister();
                super.releaseLock();
                if (getLockFile().exists()) {
                    final boolean isDeleted = getLockFile().delete();
                    //assert isDeleted : getLockFile().getAbsolutePath();
                }
            }
        }
    }
    
    private final boolean deregister() {
        return allLocks.remove(getLockFile().getAbsolutePath());
    }
    
    private final boolean register() {
        return allLocks.add(getLockFile().getAbsolutePath());
    }
    
    final File getLockFile() {
        return lockFile;
    }
    
    public final String toString() {
        final String name = getLockFile().getName();
        final String newName = name.substring(WriteLockUtils.PREFIX.length(), (name.length() - WriteLockUtils.SUFFIX.length()));
        return new File(getLockFile().getParentFile(), newName).getAbsolutePath();
    }
    
    private static class NioLock extends WriteLock {
        private FileChannel channel;
        private java.nio.channels.FileLock nioLock;
        
        NioLock(File lockFile, FileChannel channel, java.nio.channels.FileLock nioLock) {
            super(lockFile);
            
            assert nioLock != null;
            assert nioLock.isValid();
            assert channel != null;
            
            this.channel = channel;
            this.nioLock = nioLock;
        }
        
        public void releaseLock() {
            synchronized (WriteLockFactory.class) {
                try {
                    if (nioLock.isValid()) {
                        nioLock.release();
                    }
                    
                    if (channel.isOpen()) {
                        channel.close();
                    }
                } catch (java.io.IOException e) {
                    ErrorManager.getDefault().notify(e);
                }
                
                super.releaseLock();
            }
        }
    }
}
