/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.tools.license;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.io.FileWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;

/**
 * A utility which scans all java source files in the cvs tree and validates
 * the license header prior to the package statement for headers that match
 * those declared in thirdparty/licenses/thirdparty-licenses.xml
 * 
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.1.2.2 $
 */
public class ValidateLicenseHeaders
{
   static final String DEFAULT_HEADER = "/*\n" +
      "* JBoss, Home of Professional Open Source\n" +
      "*\n" +
      "* Distributable under LGPL license.\n" +
      "* See terms of license at gnu.org.\n" +
      "*/\n";
   static Logger log = Logger.getLogger("ValidateCopyrightHeaders");
   static FileFilter dotJavaFilter = new DotJavaFilter();
   /**
    * The term-headers from the thirdparty/license/thirdparty-licenses.xml
    */ 
   static HashMap licenseHeaders = new HashMap();
   /**
    * Java source files with no license header
    */ 
   static ArrayList noheaders = new ArrayList();
   /**
    * Java source files with a header that does not match one from licenseHeaders
    */ 
   static ArrayList invalidheaders = new ArrayList();
   static int totalCount;

   /**
    * ValidateLicenseHeaders jboss-src-root
    * @param args
    */ 
   public static void main(String[] args)
      throws Exception
   {
      if( args.length != 1 || args[0].startsWith("-h") )
      {
         log.info("Usage: ValidateLicenseHeaders jboss-src-root");
         System.exit(1);
      }
      File jbossSrcRoot = new File(args[0]);
      if( jbossSrcRoot.exists() == false )
      {
         log.info("Src root does not exist, check "+jbossSrcRoot.getAbsolutePath());
         System.exit(1);
      }

      // Load the valid copyright statements for the licenses
      File licenseInfo = new File(jbossSrcRoot, "thirdparty/licenses/thirdparty-licenses.xml");
      if( licenseInfo.exists() == false )
      {
         log.severe("Failed to find the thirdparty/licenses/thirdparty-licenses.xml under the src root");
         System.exit(1);
      }
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = factory.newDocumentBuilder();
      Document doc = db.parse(licenseInfo);
      NodeList licenses = doc.getElementsByTagName("license");
      for(int i = 0; i < licenses.getLength(); i ++)
      {
         Element license = (Element) licenses.item(i);
         String key = license.getAttribute("id");
         ArrayList headers = new ArrayList();
         licenseHeaders.put(key, headers);
         NodeList copyrights = license.getElementsByTagName("terms-header");
         for(int j = 0; j < copyrights.getLength(); j ++)
         {
            Element copyright = (Element) copyrights.item(j);
            copyright.normalize();
            String id = copyright.getAttribute("id");
            String text = getElementContent(copyright);
            if( text == null )
               continue;
            // Replace all duplicate whitespace with a single space
            text = text.replaceAll("[\\s*]+", " ");
            if( text.length() == 1)
               continue;

            text = text.toLowerCase();
            LicenseHeader lh = new LicenseHeader(id, text);
            headers.add(lh);
         }
      }
      log.fine(licenseHeaders.toString());

      File[] files = jbossSrcRoot.listFiles(dotJavaFilter);
      log.info("Root files count: "+files.length);
      processSourceFiles(files, 0);

      log.info("Processed "+totalCount);
      log.info("Files with no headers: "+noheaders.size());
      FileWriter fw = new FileWriter("NoHeaders.txt");
      for(Iterator iter = noheaders.iterator(); iter.hasNext();)
      {
         File f = (File) iter.next();
         fw.write(f.getAbsolutePath());
         fw.write('\n');
      }
      fw.close();
      log.info("Files with invalid headers: "+invalidheaders.size());
      fw = new FileWriter("InvalidHeaders.txt");
      for(Iterator iter = invalidheaders.iterator(); iter.hasNext();)
      {
         File f = (File) iter.next();
         fw.write(f.getAbsolutePath());
         fw.write('\n');
      }
      fw.close();
   }

   /**
    * Get all non-comment content from the element.
    * @param element
    * @return
    */ 
   public static String getElementContent(Element element)
   {
      if (element == null)
         return null;

      NodeList children = element.getChildNodes();
      StringBuffer result = new StringBuffer();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node child = children.item(i);
         if (child.getNodeType() == Node.TEXT_NODE || 
             child.getNodeType() == Node.CDATA_SECTION_NODE)
         {
            result.append(child.getNodeValue());
         }
         else if( child.getNodeType() == Node.COMMENT_NODE )
         {
            // Ignore comment nodes
         }
         else
         {
            result.append(child.getFirstChild());
         }
      }
      return result.toString().trim();
   }

   /**
    * Validate the headers of all java source files
    * 
    * @param files
    * @param level
    * @throws IOException
    */ 
   static void processSourceFiles(File[] files, int level)
      throws IOException
   {
      for(int i = 0; i < files.length; i ++)
      {
         File f = files[i];
         if( level == 0 )
            log.info("processing "+f);
         if( f.isDirectory() )
         {
            File[] children = f.listFiles(dotJavaFilter);
            processSourceFiles(children, level+1);
         }
         else
         {
            parseHeader(f);
         }
      }
   }

   /**
    * Read the first comment upto the package ...; statement
    * @param javaFile
    */ 
   static void parseHeader(File javaFile)
      throws IOException
   {
      totalCount ++;
      FileReader fr = new FileReader(javaFile);
      BufferedReader br = new BufferedReader(fr);
      String line = br.readLine();
      StringBuffer tmp = new StringBuffer();
      while( line != null )
      {
         line = line.trim();
         if( line.startsWith("package") || line.startsWith("import") )
            break;

         // Ignore any single line comments
         if( line.startsWith("//") )
         {
            line = br.readLine();
            continue;
         }

         if( line.startsWith("/**") )
            tmp.append(line.substring(3));
         else if( line.startsWith("/*") )
            tmp.append(line.substring(2));
         else if( line.startsWith("*") )
            tmp.append(line.substring(1));
         else
            tmp.append(line);
         tmp.append(' ');
         line = br.readLine();
      }
      br.close();
      fr.close();

      if( tmp.length() == 0 )
      {
         addDefaultHeader(javaFile);
         return;
      }

      String text = tmp.toString();
      // Replace all duplicate whitespace with a single space
      text = text.replaceAll("[\\s*]+", " ");
      if( tmp.length() == 1 )
      {
         addDefaultHeader(javaFile);
         return;
      }
      text = text.toLowerCase();
      // Search for a matching header
      boolean matches = false;
      Iterator iter = licenseHeaders.values().iterator();
      escape:
      while( iter.hasNext() )
      {
         List list = (List) iter.next();
         Iterator jiter = list.iterator();
         while( jiter.hasNext() )
         {
            LicenseHeader lh = (LicenseHeader) jiter.next();
            if( text.startsWith(lh.text) )
            {
               matches = true;
               if( log.isLoggable(Level.FINE) )
                  log.fine(javaFile+" matches copyright "+lh.id);
               break escape;
            }
            else if( javaFile.getName().equals("CompressionConstants.java") )
            {
               log.fine("Does not match: "+lh.id);
            }
         }
      }
      text = null;
      tmp.setLength(0);
      if( matches == false )
         invalidheaders.add(javaFile);
   }

   /**
    * Add the default jboss lgpl header
    */ 
   static void addDefaultHeader(File javaFile)
      throws IOException
   {
      FileInputStream fis = new FileInputStream(javaFile);
      FileChannel fc = fis.getChannel();
      int size = (int) fc.size();
      ByteBuffer contents = ByteBuffer.allocate(size);
      fc.read(contents);
      fis.close();
      
      ByteBuffer hdr = ByteBuffer.wrap(DEFAULT_HEADER.getBytes());
      FileOutputStream fos = new FileOutputStream(javaFile);
      fos.write(hdr.array());
      fos.write(contents.array());
      fos.close();

      noheaders.add(javaFile);
   }

   /**
    * A class that encapsulates the license id and valid terms header
    */ 
   static class LicenseHeader
   {
      String id;
      String text;
      LicenseHeader(String id, String text)
      {
         this.id = id;
         this.text = text;
      }
   }
   /**
    * A filter which accepts files ending in .java (but not _Stub.java), or
    * directories other than gen-src and gen-parsers
    */ 
   static class DotJavaFilter implements FileFilter
   {
      public boolean accept(File pathname)
      {
         boolean accept = false;
         String name = pathname.getName();
         if( pathname.isDirectory() )
         {
            // Ignore the gen-src directories for generated output
            accept = name.equals("gen-src") == false
               && name.equals("gen-parsers") == false;
         }
         else
         {
            accept = name.endsWith("_Stub.java") == false && name.endsWith(".java");
         }
         
         return accept;
      }
   }
}