/**
 *  Baffle Project
 *  The MIT License (MIT) Copyright (Baffle) 2015 guye
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 *  and associated documentation files (the "Software"), to deal in the Software 
 *  without restriction, including without limitation the rights to use, copy, modify, 
 *  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 
 *  permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in all copies 
 *  or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 *  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 *  PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
 *  FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 *  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

 *  @author guye <[email protected]>
 *
 **/
package com.guye.baffle.obfuscate;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;

import com.guye.baffle.config.BaffleConfig;
import com.guye.baffle.config.ConfigReader;
import com.guye.baffle.config.MappingWriter;
import com.guye.baffle.decoder.ArscData;
import com.guye.baffle.decoder.StringBlock;
import com.guye.baffle.exception.BaffleException;
import com.guye.baffle.util.ApkFileUtils;
import com.guye.baffle.util.LEDataOutputStream;
import com.guye.baffle.util.OS;
import com.guye.baffle.util.ZipInfo;

public class Obfuscater {

	public static final String LOG_NAME = "BAFFLE";
	private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");

	private List<ZipInfo> mZipinfos;
	private ObfuscateHelper mObfuscateHelper;
	private ArscData mArscData;

	private BaffleConfig mBaffleConfig;

	private MappingWriter mMappingWrite;

	private File[] mConfigFiles;

	private File mApkFile;

	private String mTarget;

	private File mMappingFile;

	private File mRepeatFile;

	private Logger log = Logger.getLogger(LOG_NAME);

	private Map<String, String> mWebpMapping = new HashMap<>(1000);
    private boolean toWebp;
    private int minLevelInt;
    private boolean hasOption;
    private boolean removeSameFile;

	public Obfuscater(File[] configs, File mappingFile, File repeatFile, File apkFile, String target) {
		mConfigFiles = configs;
		mApkFile = apkFile;
		mTarget = target;
		mRepeatFile = repeatFile;
		mMappingFile = mappingFile;
	}

	private StringBlock createStrings(StringBlock orgTableStrings, boolean isTableString) {
		try {
			ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
			LEDataOutputStream dataOutputStream = new LEDataOutputStream(arrayOutputStream);
			int count = orgTableStrings.getCount();
			int curOffset = 0;
			int[] offset = new int[count];
			String newStr;
			byte[] strData;
			byte[] l = new byte[2];
			byte[] l1 = new byte[2];
			int offsetLen = 1;
			int offsetDataLen = 1;
			for (int i = 0; i < count; i++) {
				if (isTableString) {
					newStr = mObfuscateHelper.getNewTableString(orgTableStrings.getString(i));
				} else {
					newStr = mObfuscateHelper.getNewKeyString(orgTableStrings.getString(i));
				}
				strData = newStr.getBytes(UTF_8_CHARSET);
				offset[i] = curOffset;
				if (newStr.length() < 128) {
					offsetLen = 1;
					l[0] = (byte) (0x7f & (newStr.length()));
				} else {
					offsetLen = 2;
					short len = (short) (newStr.length());
					l[0] = (byte) ((byte) ((len & 0xff00) >> 8) | 0x80);
					l[1] = (byte) (len & 0x00ff);
				}

				if (strData.length < 128) {
					l1[0] = (byte) (0x7f & (strData.length));
					offsetDataLen = 1;
				} else {
					offsetDataLen = 2;
					short len = (short) (strData.length);
					l1[0] = (byte) ((byte) ((len & 0xff00) >> 8) | 0x80);
					l1[1] = (byte) (len & 0x00ff);
				}
				dataOutputStream.write(l, 0, offsetLen);
				dataOutputStream.write(l1, 0, offsetDataLen);
				dataOutputStream.write(strData);
				dataOutputStream.write(0);
				curOffset += (offsetLen + offsetDataLen + strData.length + 1);
			}

			strData = arrayOutputStream.toByteArray();
			dataOutputStream.close();
			arrayOutputStream.close();

			return new StringBlock(offset, strData, orgTableStrings.getStyleOffset(), orgTableStrings.getStyle(), true);
		} catch (IOException e) {// not a disk IO option
			e.printStackTrace();
		}
		return null;
	}

	public void obfuscate() throws IOException, BaffleException {

		String tempDir = System.getProperty("java.io.tmpdir") + File.separator + "old" + File.separator;
		log.log(Level.CONFIG, "tempDir:::" + tempDir);
		File temp = new File(tempDir);
		OS.rmdir(temp);
		temp.mkdirs();

		// read keep and mapping config
		mBaffleConfig = new ConfigReader().read(mConfigFiles);

		mObfuscateHelper = new ObfuscateHelper(mBaffleConfig);

		// unzip apk or ap_ file
		List<ZipInfo> zipinfos = ApkFileUtils.unZipApk(mApkFile, tempDir, toWebp ,mWebpMapping,minLevelInt);

		if(removeSameFile){
		    removeEqualFile(temp, zipinfos);
		}
		

		// decode arsc file
		mArscData = ArscData.decode(new File(tempDir + "resources.arsc"));

		// do obfuscate
		mObfuscateHelper.obfuscate(mArscData);

		mObfuscateHelper.setWebpMapping(mWebpMapping);

		// write mapping file
		if (mMappingFile != null)

		{
			mMappingWrite = new MappingWriter(mObfuscateHelper.getObfuscateData().keyMaping);
			mMappingWrite.WriteToFile(mMappingFile);
		} else {
			log.log(Level.CONFIG, "not specific mapping file");
		}

		StringBlock tableBlock = createStrings(mArscData.getmTableStrings(), true);
		StringBlock keyBlock = createStrings(mArscData.getmSpecNames(), false);
		File arscFile = new File(tempDir + "resources.n.arsc");
		CRC32 arscCrc = mArscData.createObfuscateFile(tableBlock, keyBlock, arscFile);

		mZipinfos = zipinfos;

		ZipInfo arscInfo = new ZipInfo("resources.arsc", ZipEntry.STORED, arscFile.length(), arscCrc.getValue(), "");

		try {
			new ApkBuilder(mObfuscateHelper, mZipinfos, arscInfo).reBuildapk(mTarget, tempDir);
		} catch (IOException e) {
			OS.rmfile(mTarget);
			throw e;
		}

		OS.rmdir(temp);
	}

    private void removeEqualFile( File temp, List<ZipInfo> zipinfos ) throws FileNotFoundException {
        Map<String, String> changeEqualFile = new HashMap<>(100);

		PrintStream printStream = null;
		if (mRepeatFile != null) {
			printStream = new PrintStream(mRepeatFile);
		}
		List<ZipInfo> sortedZipinfo = new ArrayList<ZipInfo>(zipinfos.size());
		sortedZipinfo.addAll(zipinfos);

		Collections.sort(sortedZipinfo, new Comparator<ZipInfo>() {

			@Override
			public int compare(ZipInfo o1, ZipInfo o2) {
				return o1.getDigest().compareTo(o2.getDigest());
			}
		});

		int size = zipinfos.size();
		int index = 0;
		ZipInfo info = null;
		info = sortedZipinfo.get(index);
		Map<String, List<ZipInfo>> map = new HashMap<String, List<ZipInfo>>();
		while (index < size - 1) {
			if (info.getDigest().equals(sortedZipinfo.get(index + 1).getDigest())) {
				List<ZipInfo> infos = map.get(info.getDigest());
				if (infos == null) {
					infos = new ArrayList<ZipInfo>();
					map.put(info.getDigest(), infos);
					infos.add(info);
				}
				infos.add(sortedZipinfo.get(index + 1));
				index += 1;
				if (index >= size) {
					break;
				}
				info = sortedZipinfo.get(index);
			} else {
				index += 1;
				if (index >= size) {
					break;
				}
				info = sortedZipinfo.get(index);
			}
		}
		Set<Entry<String, List<ZipInfo>>> entries = map.entrySet();
		for (Entry<String, List<ZipInfo>> entry : entries) {
			if (mRepeatFile != null) {
				printStream.println("md5:" + entry.getKey());
			}
			String firstFile = null;
			for (ZipInfo z : entry.getValue()) {
				if(!z.getOrginName().startsWith("res/")){
					continue;
				}
				if (firstFile == null) {
					firstFile = z.getOrginName();
				}else{
					File dFile = new File(temp,z.getOrginName());
					dFile.delete();
					System.out.println(z.getOrginName());
					zipinfos.remove(z);
				}
				
				changeEqualFile.put(z.getOrginName(), firstFile);	
				
				if (mRepeatFile != null) {
					printStream.println("\t" + z.getOrginName());
				}
			}
			if (mRepeatFile != null) {
				printStream.println("----------");
			}
		}
		if (mRepeatFile != null) {
			printStream.close();
		}
		
		mObfuscateHelper.setChangeEqualMapping(changeEqualFile);
    }

    public void setWebpParam( boolean webp, String minLevel ) {
        this.toWebp = webp;
        if(toWebp){
            try{
                minLevelInt = Integer.parseInt(minLevel);
            }catch(Exception e){
                throw new RuntimeException("can not parse webp mini sdk level");
            }
        }
        
    }

    public void setRemoveSameFile( boolean removeSameFile ) {
        this.removeSameFile = removeSameFile;


    }

}