package com.limegroup.gnutella.malware;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
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.BEncoder;
import org.limewire.util.Base32;
import org.limewire.util.StringUtils;

/**
 * Encoder/decoder for the base32-encoded, deflated, bencoded string that
 * defines mime types for the FileExtensionChecker.
 * 
 * The bencoded data consists of a map with three keys, "m", "o", and "p",
 * which point to three 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 "o" list is a list of integers, which are
 * offsets from the beginning (positive values) or end (negative values) of the
 * file. The corresponding element in the "p" list is a list of byte patterns
 * that occur at those offsets in files of the given mime type.
 * 
 * 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 MimeTypeEncoder {

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

    private final MimeType[] mimeTypes;

    MimeTypeEncoder() {
        MimeType[] mt;
        try {
            mt = decodeSetting(FilterSettings.MIME_TYPES.get());
        } catch(InvalidDataException e) {
            mt = new MimeType[0];
        }
        mimeTypes = mt;
    }

    /**
     * Returns the mime type of a file, or null if the mime type is unknown.
     */
    String getMimeType(File file) {
        RandomAccessFile f = null;
        try {
            long length = file.length();
            f = new RandomAccessFile(file, "r");
            for(MimeType mimeType : mimeTypes) {
                boolean matches = true;
                for(int i = 0; i < mimeType.offsets.length; i++) {
                    long offset = mimeType.offsets[i];
                    int patternLength = mimeType.patterns[i].length;
                    // Negative offsets are relative to the end of the file
                    if(offset < 0)
                        offset = length - offset;
                    if(offset < 0 || offset + patternLength > length) {
                        matches = false;
                        break;
                    }
                    f.seek(offset);
                    byte[] buf = new byte[patternLength];
                    f.readFully(buf);
                    if(!Arrays.equals(buf, mimeType.patterns[i])) {
                        matches = false;
                        break;
                    }
                }
                if(matches) {
                    if(LOG.isDebugEnabled())
                        LOG.debug("Mime type " + mimeType.name);
                    return mimeType.name;
                }
            }
            LOG.debug("Unknown mime type");
            return null;
        } catch(IOException e) {
            LOG.debug("Error reading file", e);
            return null;
        } finally {
            IOUtils.close(f);
        }
    }

    /**
     * Encodes a string defining mime types. 
     */
    static String encodeSetting(MimeType[] types) throws InvalidDataException {
        List<String> names = new ArrayList<String>();
        List<List<Long>> offsets = new ArrayList<List<Long>>();
        List<List<byte[]>> patterns = new ArrayList<List<byte[]>>();
        HashMap<String, List<?>> payload = new HashMap<String, List<?>>();
        payload.put("m", names);
        payload.put("o", offsets);
        payload.put("p", patterns);

        for(MimeType m : types) {
            names.add(m.name);
            offsets.add(Arrays.asList(m.offsets));
            patterns.add(Arrays.asList(m.patterns));
        }

        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 mime types. 
     */
    static MimeType[] decodeSetting(String setting) throws InvalidDataException {
        if(setting.isEmpty())
            return new MimeType[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<?> o = (List)map.get("o");
            List<?> p = (List)map.get("p");
            if(m == null || o == null || p == null)
                throw new InvalidDataException("Missing key");
            int length = m.size();
            if(o.size() != length || p.size() != length)
                throw new InvalidDataException("List lengths differ");
            MimeType[] types = new MimeType[length];
            for(int i = 0; i < length; i++) {
                String name = StringUtils.getUTF8String((byte[])m.get(i));
                if(LOG.isDebugEnabled())
                    LOG.debug("Name: " + name);
                List<?> offsets = (List)o.get(i);
                List<?> patterns = (List)p.get(i);
                if(offsets.size() != patterns.size())
                    throw new InvalidDataException("List lengths differ");
                // Don't allow empty offset lists
                if(offsets.isEmpty())
                    throw new InvalidDataException("Empty offset list");
                Long[] off = new Long[offsets.size()];
                byte[][] pat = new byte[patterns.size()][];
                for(int j = 0; j < off.length; j++) {
                    off[j] = (Long)offsets.get(j);
                    pat[j] = (byte[])patterns.get(j);
                    if(LOG.isDebugEnabled())
                        LOG.debug("Offset: " + off[j] + ", pattern: " +
                                StringUtils.toHexString(pat[j]));
                }
                types[i] = new MimeType(name, off, pat);
            }
            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);
        }
    }
}
