/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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.
 *
 * muCommander 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.mucommander.ui.action;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.swing.KeyStroke;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

import com.mucommander.RuntimeConstants;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.io.backup.BackupInputStream;

/**
 * This class is responsible for reading the actions.
 * it read and parse the two action files - the default one 
 * (placed in the jar file) and the user's one.
 * 
 * @author Maxence Bernard, Arik Hadas
 */
class ActionKeymapReader extends ActionKeymapIO {
	private static final Logger LOGGER = LoggerFactory.getLogger(ActionKeymapReader.class);
	
	/** Maps action Class onto Keystroke instances*/
    private Map<ActionId, KeyStroke> primaryActionsReadKeymap;
    /** Maps action Class instances onto Keystroke instances*/
    private Map<ActionId, KeyStroke> alternateActionsReadKeymap;

    /** Parsed file */
    private AbstractFile file;
    
    /**
     * Loads the action file: loads the one contained in the JAR file first, and then the user's one.
     * This means any new action in the JAR action keymap (when a new version gets released) will have the default
     * keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override
     * the ones from the JAR action keymap.
     *
     * Starts parsing the XML actions file.
     * 
     * @throws ParserConfigurationException 
     * @throws IOException 
     * @throws SAXException 
     */
    ActionKeymapReader(AbstractFile file) throws SAXException, IOException, ParserConfigurationException {
    	this.file = file;
    	
    	InputStream in = null;
    	try {SAXParserFactory.newInstance().newSAXParser().parse(in = new BackupInputStream(file), this);}
    	finally {
    		if(in!=null) {
    			try { in.close(); }
    			catch(IOException e) {}
    		}
    	}
    }
	
    /**
     * Parses the keystrokes defined in the given attribute map (if any) and associates them with the given action id.
     * The keystroke will not be associated in any of the following cases:
     * <ul>
     *  <li>the keystrokes attributes do not contain any value.</li>
     *  <li>the keystrokes attributes have values that do not represent a valid KeyStroke (syntax error).</li>
     * </ul>
     * If a given keystroke is already associated to an action, the existing association is replaced. 
     * If there is a valid alternative keystroke defined but there is no valid primary keystroke defined, the primary keystroke 
     * is replaced by the alternative keystroke.
     *
     * @param actionId the action id to associate the keystroke with
     * @param attributes the attributes map that holds the value
     */
    private void processKeystrokeAttribute(ActionId actionId, Attributes attributes) {    	
    	String keyStrokeString;
    	KeyStroke alternateKeyStroke = null;
    	KeyStroke primaryKeyStroke = null;
    	
    	// Parse the primary keystroke and retrieve the corresponding KeyStroke instance
    	keyStrokeString = attributes.getValue(PRIMARY_KEYSTROKE_ATTRIBUTE);
    	
    	if (keyStrokeString != null) {
    		primaryKeyStroke = KeyStroke.getKeyStroke(keyStrokeString);
    		if (primaryKeyStroke == null)
    			LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString);
    		else {
    			ActionId prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(primaryKeyStroke);
    			if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId))
    				LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId);
    		}
    	}

    	// Parse the alternate keystroke and retrieve the corresponding KeyStroke instance
    	keyStrokeString = attributes.getValue(ALTERNATE_KEYSTROKE_ATTRIBUTE);
    	
    	if (keyStrokeString != null) {
    		alternateKeyStroke = KeyStroke.getKeyStroke(keyStrokeString);
    		if (alternateKeyStroke == null)
    			LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString);
    		else {
    			ActionId prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(alternateKeyStroke);
    			if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId))
    				LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId);
    		}
    	}

   		// If there is no primary shortcut defined for the action but there is an alternative shortcut defined,
   		// turn the alternative shortcut to the action's primary shortcut
   		if (primaryKeyStroke == null) {
   			LOGGER.debug("Action \"" + actionId +"\" has an alternative shortcut with no primary shortcut, so the alternative shortcut become primary");
   			primaryActionsReadKeymap.put(actionId, alternateKeyStroke);
   			alternateActionsReadKeymap.put(actionId, null);
   			// Mark that the actions keymap file should be updated
  			setModified();
   		}
   		else {
   			primaryActionsReadKeymap.put(actionId, primaryKeyStroke);
   			alternateActionsReadKeymap.put(actionId, alternateKeyStroke);    		
   		}
    }

    ///////////////////
    ///// getters /////
    ///////////////////
    
    public Map<ActionId, KeyStroke> getPrimaryActionsKeymap() {return primaryActionsReadKeymap;}
    
    public Map<ActionId, KeyStroke> getAlternateActionsKeymap() {return alternateActionsReadKeymap;}
    
    ///////////////////////////////////
    // ContentHandler implementation //
    ///////////////////////////////////

    @Override
    public void startDocument() {
    	LOGGER.trace(file.getAbsolutePath()+" parsing started");
    	
    	primaryActionsReadKeymap = new HashMap<ActionId, KeyStroke>();
    	alternateActionsReadKeymap = new HashMap<ActionId, KeyStroke>();
    }
    
    @Override
    public void endDocument() {
    	LOGGER.trace(file.getAbsolutePath()+" parsing finished");
    }
    
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
    	if(qName.equals(ACTION_ELEMENT)) {
    		// Retrieve the action id
    		ActionId actionId = ActionId.asGenericAction(attributes.getValue(ID_ATTRIBUTE));
    		// if id attribute not exits, read class attribute
    		if (actionId == null) {
    			String actionClassPath = attributes.getValue(CLASS_ATTRIBUTE);
    			
    			if(actionClassPath==null) {
    				LOGGER.warn("Error in action keymap file: no 'class' or 'id' attribute specified in 'action' element");
        			return;
        		}
    			// extrapolate the action id from its class path
    			actionId = ActionId.asGenericAction(ActionManager.extrapolateId(actionClassPath));
    		}
    		
    		if (!ActionManager.isActionExist(actionId)) {
    			LOGGER.warn("Error in action keymap file: could not resolve action "+actionId);
    			return;
    		}

    		// Load the action's accelerators (if any)
    		processKeystrokeAttribute(actionId, attributes);
    	}
    	else if (qName.equals(ROOT_ELEMENT)) {
    		// Note: early 0.8 beta3 nightly builds did not have version attribute, so the attribute may be null
            String fileVersion = attributes.getValue(VERSION_ATTRIBUTE);
    		
    		// if the file's version is not up-to-date, update the file to the current version at quitting.
    		if (!RuntimeConstants.VERSION.equals(fileVersion))
    			setModified();
    	}
    }
}
