package com.googlecode.ant_deb_task;

import org.apache.tools.ant.*;
import org.apache.tools.ant.types.EnumeratedAttribute;

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

/**
 * Task that generates a desktop entry file used to add menu entries.
 *
 * @antTaskName desktopentry
 */
public class DesktopEntry extends Task
{
    private Properties _categories = new Properties ();
    private Properties _keys = new Properties ();

    private File _toFile;

    private Map _entries;
    private List _localizedEntries = new ArrayList ();

    private static final Set VALID_ONLY_SHOW_IN = new HashSet (
        Arrays.asList (new String[] {"GNOME", "KDE", "ROX", "XFCE", "Old", "Unity", "LXDE"}));

    public static class LocalizedEntry
    {
        private String _key;
        private String _lang;
        private String _country;
        private String _encoding;
        private String _modifier;
        private String _value;

        public LocalizedEntry (String key)
        {
            _key = key;
        }

        public void setLang (String lang)
        {
            _lang = lang;
        }

        public void setCountry (String country)
        {
            _country = country;
        }

        public void setEncoding (String encoding)
        {
            _encoding = encoding;
        }

        public void setModifier (String modifier)
        {
            _modifier = modifier;
        }

        public void setValue (String value)
        {
            _value = value;
        }

        public String toString()
        {
            StringBuffer buffer = new StringBuffer (_key);

            if (_lang != null)
            {
                buffer.append ('[');

                buffer.append (_lang.toLowerCase ());

                if (_country != null)
                {
                    buffer.append ('_');
                    buffer.append (_country.toUpperCase ());
                }

                if (_encoding != null)
                {
                    buffer.append ('.');
                    buffer.append (_encoding);
                }

                if (_modifier != null)
                {
                    buffer.append ('@');
                    buffer.append (_modifier);
                }

                buffer.append (']');
            }

            return buffer.toString ();
        }

        public String getValue ()
        {
            return _value;
        }
    }

    public static class Name extends LocalizedEntry
    {
        public Name ()
        {
            super ("Name");
        }
    }

    public static class GenericName extends LocalizedEntry
    {
        public GenericName ()
        {
            super ("GenericName");
        }
    }

    public static class Comment extends LocalizedEntry
    {
        public Comment ()
        {
            super ("Comment");
        }
    }

    public static class Icon extends LocalizedEntry
    {
        public Icon ()
        {
            super ("Icon");
        }
    }

    public static class Type extends EnumeratedAttribute
    {
        public String[] getValues ()
        {
            return new String[] {"Application", "Link", "Directory"};
        }
    }

    /**
     * The file where the desktop entry will be generated. The file name extension should be .desktop.
     *
     * @param toFile The file name, if exisits it will be overwritten.
     * @antTaskParamRequired true
     */
    public void setToFile(File toFile)
    {
        _toFile = toFile;
    }

    public void setType (Type type)
    {
        String typeValue = type.getValue();

        _entries.put("Type", typeValue);

        if (!typeValue.equals("Application"))
            _entries.remove("Terminal");
    }

    public void setName(String name)
    {
        _entries.put("Name", name);
    }

    public void setGenericName(String genericName)
    {
        _entries.put("GenericName", genericName);
    }

    public void setNoDisplay(boolean noDisplay)
    {
        _entries.put("NoDisplay", noDisplay ? "true" : "false");
    }

    public void setComment(String comment)
    {
        _entries.put("Comment", comment);
    }

    public void setIcon(String icon)
    {
        _entries.put("Icon", icon);
    }

    public void setOnlyShowIn(String onlyShowIn)
    {
        if (_entries.containsKey ("NotShowIn"))
            throw new BuildException("Only one of either OnlyShowIn or NotShowIn can be set!");

        String [] categories = onlyShowIn.split (";");
        for (int i = 0; i < categories.length; i++)
        {
            String category = categories[i];

            if (!VALID_ONLY_SHOW_IN.contains (category))
                throw new BuildException(category + " is not a valid OnlyShowIn category!");
        }

        _entries.put("OnlyShowIn", onlyShowIn);
    }

    public void setNotShowIn(String notShowIn)
    {
        if (_entries.containsKey ("OnlyShowIn"))
            throw new BuildException("Only one of either OnlyShowIn or NotShowIn can be set!");

        String [] categories = notShowIn.split (";");
        for (int i = 0; i < categories.length; i++)
        {
            String category = categories[i];

            if (!VALID_ONLY_SHOW_IN.contains (category))
                throw new BuildException(category + " is not a valid NotShowIn category!");
        }

        _entries.put("NotShowIn", notShowIn);
    }

    public void setTryExec(String tryExec)
    {
        _entries.put("TryExec", tryExec);
    }

    public void setExec(String exec)
    {
        _entries.put("Exec", exec);
    }

    public void setPath(String path)
    {
        _entries.put("Path", path);
    }

    public void setTerminal(boolean terminal)
    {
        _entries.put("Terminal", terminal ? "true" : "false");
    }

    public void setMimeType(String mimeType)
    {
        _entries.put("MimeType", mimeType);
    }

    public void setCategories(String categories)
    {
        validateCategories (categories);

        _entries.put("Categories", categories);
    }

    public void setUrl(String url)
    {
        _entries.put("URL", url);
    }

    public void addName(DesktopEntry.Name name)
    {
        _localizedEntries.add (name);
    }

    public void addGenericName(GenericName genericName)
    {
        _localizedEntries.add (genericName);
    }

    public void addComment(Comment comment)
    {
        _localizedEntries.add (comment);
    }

    public void addIcon(Icon icon)
    {
        _localizedEntries.add (icon);
    }

    public void init() throws BuildException
    {
        _entries = new LinkedHashMap();

        _entries.put("Version", "1.0");
        _entries.put("Type", "Application");
        _entries.put("Terminal", "false");

        try
        {
            InputStream categoriesStream = getClass ().getResourceAsStream ("DesktopEntryCategories.properties");
            _categories.load (categoriesStream);
            log ("Loaded " + _categories.size () + " properties from DesktopEntryCategories", Project.MSG_VERBOSE);

            InputStream keysStream = getClass ().getResourceAsStream ("DesktopEntryKeys.properties");
            _keys.load (keysStream);
            log ("Loaded " + _keys.size () + " properties from DesktopEntryKeys", Project.MSG_VERBOSE);
        }
        catch (Exception e)
        {
            throw new BuildException("Cannot load properties file!", e);
        }
    }

    public void execute() throws BuildException
    {
        try
        {
            log ("Generating desktop entry to: " + _toFile.getAbsolutePath ());

            for (int i = 0; i < _localizedEntries.size (); i++)
            {
                LocalizedEntry localizedEntry = (LocalizedEntry) _localizedEntries.get (i);

                _entries.put (localizedEntry.toString (), localizedEntry.getValue ());
            }

            validateKeys ();

            PrintWriter out = new UnixPrintWriter(_toFile);

            out.println("[Desktop Entry]");

            Iterator keys = _entries.keySet().iterator();
            while (keys.hasNext())
            {
                String key = (String) keys.next();
                String value = (String) _entries.get(key);

                out.print(key);
                out.print('=');
                out.println(value);
            }

            out.close();
        }
        catch (FileNotFoundException e)
        {
            throw new BuildException(e);
        }
    }

    private void validateCategories(String categories)
    {
        int mainCnt = 0;

        Set categorySet = new HashSet (Arrays.asList (categories.split (";")));
        for (Iterator iterator = categorySet.iterator (); iterator.hasNext ();)
        {
            String category = (String) iterator.next ();

            String type = _categories.getProperty ("category." + category);

            if (type == null)
                throw new BuildException("Unknown category: " + category);

            if ("main".equals (type))
                mainCnt++;

            String require = _categories.getProperty ("category." + category + ".require");
            if (require != null)
            {
                if (!categorySet.contains (require))
                    throw new BuildException("Category " + category + " also requires " + require);
            }
            else
            {
                require = _categories.getProperty ("category." + category + ".require.and");

                if (require != null)
                {
                    String [] requireAnd = require.split (";");
                    for (int j = 0; j < requireAnd.length; j++)
                    {
                        if (!categorySet.contains (requireAnd[j]))
                            throw new BuildException("Category " + category + " also requires " + requireAnd[j]);
                    }
                }
                else
                {
                    require = _categories.getProperty ("category." + category + ".require.or");

                    if (require != null)
                    {
                        boolean found = false;
                        String [] requireOr = require.split (";");
                        for (int j = 0; j < requireOr.length; j++)
                        {
                            if (categorySet.contains (requireOr[j]))
                            {
                                found = true;
                                break;
                            }
                        }

                        if (!found)
                            throw new BuildException("Category " + category + " also requires one of " + require);
                    }
                }
            }
        }

        if (mainCnt == 0)
            throw new BuildException ("At least one main category should be specified!");

        if (mainCnt > 1)
            log ("Multiple main categories specified, the entry may appear under multiple menus!");
    }

    private void validateKeys()
    {
        String type = (String) _entries.get ("Type");
        Iterator keys = _entries.keySet ().iterator ();
        while (keys.hasNext ())
        {
            String key = (String) keys.next ();

            if ("Type".equals (key))
                continue;

            int idxSquare = key.indexOf ('[');

            if (idxSquare != -1)
            {
                String baseKey = key.substring (0, idxSquare);

                if (!_entries.containsKey (baseKey))
                    throw new BuildException ("Unlocalized key " + baseKey + " also needed for " + key);

                continue;
            }

            boolean found = false;
            String [] typeArr = _keys.getProperty ("key." + key).split (",");
            for (int i = 0; i < typeArr.length; i++)
            {
                String allowedType = typeArr[i];

                if (type.equals (allowedType))
                {
                    found = true;
                    break;
                }
            }

            if (!found)
                throw new BuildException("Key " + key + " not allowed for desktop entry of type " + type);
        }

        Iterator propertyKeys = _keys.keySet ().iterator ();
        while (propertyKeys.hasNext ())
        {
            String property = (String) propertyKeys.next ();

            if (property.endsWith (".required"))
            {
                String [] requiredTypeArr = _keys.getProperty (property).split (",");

                for (int i = 0; i < requiredTypeArr.length; i++)
                {
                    String requiredType = requiredTypeArr[i];

                    if (type.equals (requiredType))
                    {
                        String key = property.substring (4, property.length () - 9);

                        if (!_entries.containsKey (key))
                            throw new BuildException("Key " + key + " is required for desktop entries of type " + type);

                        break;
                    }
                }
            }
        }
    }
}

