package com.limegroup.gnutella.malware;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import org.limewire.bittorrent.bencoding.Token;
import org.limewire.core.settings.FilterSettings;
import org.limewire.io.IOUtils;
import org.limewire.io.InvalidDataException;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.util.Base32;
import org.limewire.util.BEncoder;
import org.limewire.util.StringUtils;

/**
 * Encoder/decoder for the base32-encoded, deflated, bencoded string that
 * defines dangerous file types for the FileExtensionChecker.
 * 
 * The bencoded data consists of a map with two keys, "m" and "e", which point
 * to two lists of equal length. Each element of the "m" list is a lowercase
 * UTF-8 string containing the name of a mime type. The corresponding element
 * in the "e" list is a list of file extensions, in lowercase UTF-8, without
 * leading dots.
 * 
 * A file is misnamed if it has a mime type that indicates a dangerous file
 * type but does not have one of the extensions allowed for that type. 
 */
class DangerousFileTypeEncoder {

    private static final Log LOG =
        LogFactory.getLog(DangerousFileTypeEncoder.class);

    private final DangerousFileType[] fileTypes;

    DangerousFileTypeEncoder() {
        DangerousFileType[] ft;
        try {
            ft = decodeSetting(FilterSettings.DANGEROUS_FILE_TYPES.get());
        } catch(InvalidDataException e) {
            ft = new DangerousFileType[0];
        }
        fileTypes = ft;
    }

    /**
     * Returns true if the given mime type is allowed to have the given
     * extension.
     */
    boolean isAllowed(String mimeType, String extension) {
        for(DangerousFileType fileType : fileTypes) {
            if(fileType.mimeType.equals(mimeType)) {
                if(LOG.isDebugEnabled())
                    LOG.debug("Dangerous mime type " + mimeType);
                for(String allowed : fileType.extensions) {
                    if(extension.equals(allowed)) {
                        if(LOG.isDebugEnabled())
                            LOG.debug("Safe extension " + extension);
                        return true;
                    }
                }
                if(LOG.isDebugEnabled())
                    LOG.debug("Dangerous extension " + extension);
                return false;
            }
        }
        if(LOG.isDebugEnabled())
            LOG.debug("Safe mime type " + mimeType);
        return true;
    }

    /**
     * Encodes a string defining dangerous file types. 
     */
    static String encodeSetting(DangerousFileType[] types) throws InvalidDataException {
        List<String> mimeTypes = new ArrayList<String>();
        List<List<String>> extensions = new ArrayList<List<String>>();
        HashMap<String, List<?>> payload = new HashMap<String, List<?>>();
        payload.put("m", mimeTypes);
        payload.put("e", extensions);

        for(DangerousFileType d : types) {
            mimeTypes.add(d.mimeType);
            extensions.add(Arrays.asList(d.extensions));
        }

        ByteArrayOutputStream zipped = new ByteArrayOutputStream();
        DeflaterOutputStream zip = new DeflaterOutputStream(zipped);
        try {
            BEncoder.getEncoder(zip, true, false, "UTF-8").encodeDict(payload);
            zip.flush();
        } catch(IOException e) {
            LOG.error("Error encoding setting", e);
            throw new InvalidDataException(e);
        } finally {
            IOUtils.close(zip);
        }
        return Base32.encode(zipped.toByteArray());
    }

    /**
     * Decodes a string defining dangerous file types. 
     */
    static DangerousFileType[] decodeSetting(String setting) throws InvalidDataException {
        if(setting.isEmpty())
            return new DangerousFileType[0];
        InflaterInputStream unzip = null;
        ReadableByteChannel byteChannel = null;
        try {
            ByteArrayInputStream zipped =
                new ByteArrayInputStream(Base32.decode(setting));
            unzip = new InflaterInputStream(zipped);
            byteChannel = Channels.newChannel(unzip);
            Object bencoded = Token.parse(byteChannel, "UTF-8");
            if(bencoded == null)
                throw new InvalidDataException("No bencoded object");
            Map<?, ?> map = (Map)bencoded;
            List<?> m = (List)map.get("m");
            List<?> e = (List)map.get("e");
            if(m == null || e == null)
                throw new InvalidDataException("Missing key");
            int length = m.size();
            if(e.size() != length)
                throw new InvalidDataException("List lengths differ");
            DangerousFileType[] types = new DangerousFileType[length];
            for(int i = 0; i < length; i++) {
                String mimeType = StringUtils.getUTF8String((byte[])m.get(i));
                if(LOG.isDebugEnabled())
                    LOG.debug("Mime type: " + mimeType);
                List<?> extensions = (List)e.get(i);
                String[] ext = new String[extensions.size()];
                for(int j = 0; j < ext.length; j++) {
                    ext[j] = StringUtils.getUTF8String((byte[])extensions.get(j));
                    if(LOG.isDebugEnabled())
                        LOG.debug("Extension: " + ext[j]);
                }
                types[i] = new DangerousFileType(mimeType, ext);
            }
            return types;
        } catch(ClassCastException e) {
            LOG.debug("Error decoding setting", e);
            throw new InvalidDataException(e);
        } catch(Exception e) {
            LOG.debug("Error decoding setting", e);
            throw new InvalidDataException(e);
        } finally {
            IOUtils.close(byteChannel);
            IOUtils.close(unzip);
        }
    }
}
