/**
 * Copyright (c) 2014 Etherios, a Division of Digi International Inc.
 * All rights not expressly granted are reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Etherios 11001 Bren Road East, Minnetonka, MN 55343
 * =======================================================================
 */
package com.etherios.connector.emulator.firmware;

import com.etherios.connector.IPlatformCustomization;
import com.etherios.connector.core.facilities.firmware.FirmwareConstants;
import com.etherios.connector.core.facilities.firmware.FirmwareDownloadStatus;
import com.etherios.connector.core.facilities.firmware.FirmwareTarget;
import com.etherios.connector.core.facilities.firmware.FirmwareTargetsParser;
import com.etherios.connector.core.facilities.firmware.IFirmwareUpdateImplementation;
import com.etherios.connector.core.log.Logger;
import com.etherios.connector.core.utils.ByteUtils;
import com.etherios.connector.core.utils.ChecksumUtils;
import com.etherios.connector.preferences.PreferencesManager;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;

/**
 * Firmware manager to process firmware update requests.
 * 
 * <p>This class delegates the firmware update process to the applications that 
 * use Cloud Connector. The application must implement the interface 
 * {@link IGeneralFirmwareImplementation} that must be provided to the 
 * {@link com.etherios.connector.emulator.CloudConnector} constructor.</p>
 * 
 * <p>This manager will notify the {@code IGeneralFirmwareImplementation} object
 * of the complete download of the firmware file or of the reception of every 
 * chunk of the same file. This behavior is configured in the 
 * {@code connector_config.txt} file using the parameter 
 * {@value #PARAM_FIRMWARE_FILE_NOTIFY} that can be:</p>
 * 
 * <ul>
 * <li>{@value #FIRMWARE_FILE_NOTIFY_FILE}</li>
 * <li>{@value #FIRMWARE_FILE_NOTIFY_CHUNK}</li>
 * </ul>
 * 
 * <p>The available targets to be updated via Device Cloud must be defined in a 
 * file called {@value #FIRMWARE_TARGETS_FILE} embedded in the jar file, 
 * following the format:</p>
 * 
 * <pre>
 * {@code
 * <targets>
 *   <firmware_target id="0" name="My Target">
 *       <!-- Maximum allowed size of the firmware image -->
 *       <size>2097152</size>
 *       <!-- Current version of the firmware -->
 *       <version>0.0.0.1</version>
 *       <!-- Regular expression for the firmware image name -->
 *       <regex>.*.bin</regex>
 *       <!-- Determines if the firmware can be updated or not -->
 *       <can_update>true</can_update>
 *       <!-- Path to save the firmware image if the 'firmware_file_notify' 
 *       property of the connector_config.txt is 'file_completed' -->
 *       <path>/root1</path>
 *   </firmware_target>
 *   <firmware_target id="1" name="Another Target">
 *       [...]
 *   </firmware_target>
 *</targets>}
 *</pre>
 * 
 * @see IGeneralFirmwareImplementation
 * @see IFirmwareUpdateImplementation
 */
public class GeneralFirmwareManager implements IFirmwareUpdateImplementation {

	// Constants
	private final static String FIRMWARE_TARGETS_FILE = "/firmware_targets.xml";
	
	/**
	 * 'Firmware notification' setting name: {@value}.
	 * @see #FIRMWARE_FILE_NOTIFY_FILE
	 * @see #FIRMWARE_FILE_NOTIFY_CHUNK
	 * @see #DEFAULT_FIRMWARE_FILE_NOTIFY
	 * @see #getParameterValue(java.lang.String)
	 */
	public final static String PARAM_FIRMWARE_FILE_NOTIFY = "firmware_file_notify";
	
	/**
	 * 'Firmware notification' is thrown when the firmware file is completely 
	 * downloaded.
	 * <p>The value of this constant is {@value}.</p>
	 * @see #PARAM_FIRMWARE_FILE_NOTIFY
	 * @see #FIRMWARE_FILE_NOTIFY_CHUNK
	 */
	public final static String FIRMWARE_FILE_NOTIFY_FILE = "file_completed";
	/**
	 * 'Firmware notification' is thrown every time a chunk of the firmware 
	 * file is received.
	 * <p>The value of this constant is {@value}.</p>
	 * @see #PARAM_FIRMWARE_FILE_NOTIFY
	 * @see #FIRMWARE_FILE_NOTIFY_FILE
	 */
	public final static String FIRMWARE_FILE_NOTIFY_CHUNK = "chunk_received";
	/**
	 * Default value for 'Firmware notification' setting: {@value}.
	 * @see #PARAM_FIRMWARE_FILE_NOTIFY
	 * @see #FIRMWARE_FILE_NOTIFY_FILE
	 * @see #FIRMWARE_FILE_NOTIFY_CHUNK
	 */
	public final static String DEFAULT_FIRMWARE_FILE_NOTIFY = FIRMWARE_FILE_NOTIFY_FILE;
	
	private final static String FILE_SEPARATOR = "/";
	private final static String FILE_PREFIX = "file://";
	
	// Variables
	private IPlatformCustomization platformCustomization;
	
	private String filePath;
	
	private IGeneralFirmwareImplementation generalFirmwareImpl;
	
	private OutputStream outputStream;
	
	private int fileSize;
	
	private int downloadSize;
	
	private List<FirmwareTarget> firmwareTargets;
	
	private Map<Integer, FirmwareTarget> resultTargets;
	
	private FirmwareDownloadStatus fwDownloadStatus;

	/**
	 * Class constructor. Instances a new General Firmware Manager object.
	 * 
	 * @param platformCustomization Platform customization.
	 */
	public GeneralFirmwareManager(IPlatformCustomization platformCustomization) {
		this(platformCustomization, null);
	}

	/**
	 * Class constructor. Instances a new General Firmware Manager object with 
	 * the given parameters.
	 * 
	 * @param platformCustomization Platform customization.
	 * @param generalFirmwareImpl Final application firmware implementation.
	 */
	public GeneralFirmwareManager(IPlatformCustomization platformCustomization, 
			IGeneralFirmwareImplementation generalFirmwareImpl) {
		this.platformCustomization = platformCustomization;
		this.generalFirmwareImpl = generalFirmwareImpl;
	}

	@Override
	public List<FirmwareTarget> getFirmwareTargets() {
		// Initialize list.
		if (firmwareTargets == null) {
			firmwareTargets = new ArrayList<>(0);
			// Parse the Firmware Targets xml file (if it exists) and add the parsed targets.
			InputStream stream = Class.class.getClass().getResourceAsStream(FIRMWARE_TARGETS_FILE);
			if (stream != null) {
				List<FirmwareTarget> parsedTargets = FirmwareTargetsParser.parseFirmwareTargets(stream);
				for (FirmwareTarget ft: parsedTargets)
					firmwareTargets.add(ft);
				try {
					stream.close();
				} catch (IOException e) {}
			} else {
				Logger.logSystemInfo(platformCustomization.getConnectorLogger(), 
						"File '" + FIRMWARE_TARGETS_FILE 
								+ "' not found, using auto generated targets.");
			}
		}
		return firmwareTargets;
	}

	@Override
	public int acceptUpdateRequest(int target, String version, int size, String fileName, 
			String targetLabel, String targetRegex) {
		// At this point the firmware size, file name and accept upload parameters 
		// have been already verified, create file (if required) and return true.
		String value = getParameterValue(PARAM_FIRMWARE_FILE_NOTIFY);
		if (value == null)
			value = DEFAULT_FIRMWARE_FILE_NOTIFY;
		fileSize = size;
		FirmwareTarget updateTarget = getTarget(target);
		if (value.equals(FIRMWARE_FILE_NOTIFY_FILE)) {
			String path = updateTarget.getTargetPath();
			if (path == null)
				filePath = fileName;
			else {
				if (path.length() > 0 && !path.endsWith(FILE_SEPARATOR))
					path = path + FILE_SEPARATOR;
				filePath = path + fileName;
			}
			FileConnection file = null;
			try {
				file = (FileConnection)Connector.open(FILE_PREFIX + filePath);
				if (file.exists())
					file.delete();
				file.create();
				outputStream = file.openOutputStream();
			} catch (IOException e) {
				// Log the exception.
				Logger.logException(platformCustomization.getConnectorLogger(), e);
				return DOWNLOAD_RESPONSE_TYPE_ERROR;
			} catch (SecurityException e) {
				// Log the exception.
				Logger.logException(platformCustomization.getConnectorLogger(), e);
				return DOWNLOAD_RESPONSE_TYPE_DOWNLOAD_DENIED;
			} finally {
				try {
					if (file != null)
						file.close();
				} catch (IOException e) {}
			}
		}
		
		// Store the target that is being updated.
		if (resultTargets == null)
			resultTargets = new HashMap<>(0);
		
		// Here we are considering the target label and regex are override-able.
		FirmwareTarget resultFT = new FirmwareTarget(target, version, 
				updateTarget.getAvailableSize(), 
				targetLabel.length() != 0 ? targetLabel: updateTarget.getLabel(),
				targetRegex.length() != 0 ? targetRegex: updateTarget.getRegex());
		resultFT.setAllowUpdate(updateTarget.isUpdateAllowed());
		resultFT.setTargetPath(updateTarget.getTargetPath());
		resultTargets.put(target, resultFT);
		
		if (generalFirmwareImpl != null) {
			int answer = generalFirmwareImpl.firmwareUpdateStart(updateTarget, size, 
					fileName);
			if (answer != DOWNLOAD_RESPONSE_TYPE_OK)
				removeResultTarget(target);
			return answer;
		}
		return DOWNLOAD_RESPONSE_TYPE_OK;
	}

	@Override
	public boolean handleNewDataChunk(int target, int offset, byte[] dataChunk) {
		boolean success = true;
		downloadSize = downloadSize + dataChunk.length;
		String value = getParameterValue(PARAM_FIRMWARE_FILE_NOTIFY);
		if (value == null)
			value = DEFAULT_FIRMWARE_FILE_NOTIFY;
		if (value.equals(FIRMWARE_FILE_NOTIFY_FILE)) {
			if (outputStream != null) {
				try {
					outputStream.write(dataChunk);
				} catch (IOException e) {
					removeResultTarget(target);
					if (generalFirmwareImpl != null)
						generalFirmwareImpl.firmwareUpdateError();
					// Log the exception.
					Logger.logException(platformCustomization.getConnectorLogger(), e);
					success = false;
				}
			}
		// Notify fw implementaton about the received chunk only if it exists 
		// and it is configured to receive chunk notifications.
		} else if (value.equals(FIRMWARE_FILE_NOTIFY_CHUNK)
				&& generalFirmwareImpl != null) {
			success = generalFirmwareImpl.dataChunkReceived(dataChunk);
			if (!success)
				removeResultTarget(target);
		}
		return success;
	}

	@Override
	public int downloadComplete(int target, int size, byte[] checksum) {
		int status = FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_OK;
		
		// Check the size of the downloaded data.
		if (fileSize != downloadSize){
			Logger.logSystemError(platformCustomization.getConnectorLogger(), 
					"Downloaded file size (" + downloadSize + " bytes) is not " 
							+ "equal to complete firmware file size (" 
							+ fileSize + " bytes)");
			removeResultTarget(target);
			return FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_ERROR;
		}
		
		String value = getParameterValue(PARAM_FIRMWARE_FILE_NOTIFY);
		if (value == null)
			value = DEFAULT_FIRMWARE_FILE_NOTIFY;
		if (value.equals(FIRMWARE_FILE_NOTIFY_FILE)) {
			// Reset variables.
			if (outputStream != null) {
				try {
					outputStream.close();
				} catch (IOException e) {
					removeResultTarget(target);
					if (generalFirmwareImpl != null)
						generalFirmwareImpl.firmwareUpdateError();
					// Log the exception.
					Logger.logException(platformCustomization.getConnectorLogger(), e);
					return FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_ERROR;
				}
			}
			outputStream = null;
			
			// Verify the checksum, if it is required.
			if (ByteUtils.byteArrayToInt(checksum, 0) != 0) {
				FileConnection file = null;
				try {
					file = (FileConnection)Connector.open(FILE_PREFIX + filePath);
					byte[] calculatedCRC32 = ChecksumUtils.getCRC32Checksum(
							file.openInputStream());
					if (!ByteUtils.compareByteArrays(checksum, calculatedCRC32))
						return FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_CHECKSUM_ERROR;
				} catch (Exception e) {
					removeResultTarget(target);
					Logger.logException(platformCustomization.getConnectorLogger(), e);
					return FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_CHECKSUM_ERROR;
				} finally {
					try {
						if (file != null)
							file.close();
					} catch (IOException e) {}
				}
			}
			
			// Notify if there is an implementaton waiting.
			if (generalFirmwareImpl != null 
				&& !generalFirmwareImpl.fileReceived(filePath))
					status = FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_ERROR;
		} else if (value.equals(FIRMWARE_FILE_NOTIFY_CHUNK)) {
			if (generalFirmwareImpl != null)
				status = generalFirmwareImpl.dataChunkDownloadComplete(checksum);
		}
		fileSize = 0;
		downloadSize = 0;
		if (status == FirmwareConstants.DOWNLOAD_COMPLETE_STATUS_OK) {
			// Update the target information.
			FirmwareTarget newTarget = resultTargets.get(target);
			
			FirmwareTarget oldTarget = null;
			ArrayList<FirmwareTarget> newTargets = new ArrayList<>();
			for (FirmwareTarget t: getFirmwareTargets()) {
				if (t.getTarget() == newTarget.getTarget()) {
					newTargets.add(newTarget);
					oldTarget = t;
				} else
					newTargets.add(t);
			}
			
			firmwareTargets.clear();
			firmwareTargets = null;
			// Store the updated firmware data.
			firmwareTargets = newTargets;
			fwDownloadStatus = new FirmwareDownloadStatus(newTarget.getTarget(), 
					FirmwareDownloadStatus.DOWNLOAD_STATUS_OK, 
					newTarget.getVersionAsByteArray(), 
					oldTarget.getVersionAsByteArray());
		}
		removeResultTarget(target);
		return status;
	}

	@Override
	public void downloadAbort(int target, int reason, boolean abortByServer) {
		// Reason and target of the abort does not matter here, just close 
		// streams and reset variables.
		removeResultTarget(target);
		if (outputStream != null) {
			try {
				outputStream.close();
			} catch (IOException e) {}
		}
		if (filePath != null) {
			FileConnection file = null;
			try {
				file = (FileConnection)Connector.open(FILE_PREFIX + filePath);
				if (file.exists())
					file.delete();
			} catch (IOException e) {
				// Log the exception.
				Logger.logException(platformCustomization.getConnectorLogger(), e);
			} finally {
				try {
					if (file != null)
						file.close();
				} catch (IOException e) {}
			}
		}
		outputStream = null;
		filePath = null;
		fileSize = 0;
		downloadSize = 0;
		// Notify if the abort was done by the server.
		if (generalFirmwareImpl != null) {
			if (abortByServer)
				generalFirmwareImpl.firmwareDownloadAbort(reason);
			else
				generalFirmwareImpl.firmwareUpdateError();
		}
	}

	@Override
	public void resetTarget(int target) {
		if (getTarget(target) == null)
			fwDownloadStatus = new FirmwareDownloadStatus(target, 
					FirmwareDownloadStatus.DOWNLOAD_STATUS_ERROR_BAD_TARGET_INFO, 
					null, null);
		// Reset the target...
	}

	@Override
	public boolean allowFirmwareUpdates() {
		return true;
	}

	@Override
	public FirmwareDownloadStatus getFirmwareDownloadStatus() {
		if (fwDownloadStatus == null)
			return null;
		FirmwareDownloadStatus status = fwDownloadStatus;
		// Remove the download status once it was returned.
		fwDownloadStatus = null;
		return status;
	}

	/**
	 * Retrieves the firmware target based on the given target id.
	 * 
	 * @param targetID Target id of the firmware target to retrieve.
	 * @return The firmware target with the given target ID, {@code null} if 
	 *         not found.
	 */
	private FirmwareTarget getTarget(int targetID) {
		for (FirmwareTarget target: firmwareTargets) {
			if (target.getTarget() == targetID)
				return target;
		}
		return null;
	}

	/**
	 * Retrieves the value of the given parameter name.
	 * 
	 * @param parameter the parameter name to get its value.
	 * @return a string representing the value of the given parameter name.
	 */
	public String getParameterValue(String parameter) {
		return ((PreferencesManager)platformCustomization.getConnectorPreferences()).getParameterValue(parameter);
	}

	/**
	 * Remove target that was being update from the map.
	 * 
	 * @param target the target number to be removed.
	 */
	private void removeResultTarget(int target){
		if (resultTargets == null || resultTargets.isEmpty())
			return;
		resultTargets.remove(target);
		if (resultTargets.isEmpty())
			resultTargets = null;
	}
}
