/*
 *  UVCCamera
 *  library and sample to access to UVC web camera on non-rooted Android device
 *
 * Copyright (c) 2014-2017 saki t_saki@serenegiant.com
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 *  All files in the folder are under this Apache License, Version 2.0.
 *  Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder
 *  may have a different license, see the respective files.
 */

package com.serenegiant.usb;

import android.content.Context;
import android.content.res.Resources.NotFoundException;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbInterface;
import android.text.TextUtils;
import android.util.Log;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class DeviceFilter {

	private static final String TAG = "DeviceFilter";

	// USB Vendor ID (or -1 for unspecified)
	public final int mVendorId;
	// USB Product ID (or -1 for unspecified)
	public final int mProductId;
	// USB device or interface class (or -1 for unspecified)
	public final int mClass;
	// USB device subclass (or -1 for unspecified)
	public final int mSubclass;
	// USB device protocol (or -1 for unspecified)
	public final int mProtocol;
	// USB device manufacturer name string (or null for unspecified)
	public final String mManufacturerName;
	// USB device product name string (or null for unspecified)
	public final String mProductName;
	// USB device serial number string (or null for unspecified)
	public final String mSerialNumber;
	// set true if specific device(s) should exclude
	public final boolean isExclude;

	public DeviceFilter(final int vid, final int pid, final int clasz, final int subclass,
			final int protocol, final String manufacturer, final String product, final String serialNum) {
		this(vid, pid, clasz, subclass, protocol, manufacturer, product, serialNum, false);
	}

	public DeviceFilter(final int vid, final int pid, final int clasz, final int subclass,
			final int protocol, final String manufacturer, final String product, final String serialNum, final boolean isExclude) {
		mVendorId = vid;
		mProductId = pid;
		mClass = clasz;
		mSubclass = subclass;
		mProtocol = protocol;
		mManufacturerName = manufacturer;
		mProductName = product;
		mSerialNumber = serialNum;
		this.isExclude = isExclude;
/*		Log.i(TAG, String.format("vendorId=0x%04x,productId=0x%04x,class=0x%02x,subclass=0x%02x,protocol=0x%02x",
			mVendorId, mProductId, mClass, mSubclass, mProtocol)); */
	}

	public DeviceFilter(final UsbDevice device) {
		this(device, false);
	}

	public DeviceFilter(final UsbDevice device, final boolean isExclude) {
		mVendorId = device.getVendorId();
		mProductId = device.getProductId();
		mClass = device.getDeviceClass();
		mSubclass = device.getDeviceSubclass();
		mProtocol = device.getDeviceProtocol();
		mManufacturerName = null;	// device.getManufacturerName();
		mProductName = null;		// device.getProductName();
		mSerialNumber = null;		// device.getSerialNumber();
		this.isExclude = isExclude;
/*		Log.i(TAG, String.format("vendorId=0x%04x,productId=0x%04x,class=0x%02x,subclass=0x%02x,protocol=0x%02x",
			mVendorId, mProductId, mClass, mSubclass, mProtocol)); */
	}

	/**
	 * 指定したxmlリソースからDeviceFilterリストを生成する
	 * @param context
	 * @param deviceFilterXmlId
	 * @return
	 */
	public static List<DeviceFilter> getDeviceFilters(final Context context, final int deviceFilterXmlId) {
		final XmlPullParser parser = context.getResources().getXml(deviceFilterXmlId);
		final List<DeviceFilter> deviceFilters = new ArrayList<DeviceFilter>();
		try {
			int eventType = parser.getEventType();
			while (eventType != XmlPullParser.END_DOCUMENT) {
	            if (eventType == XmlPullParser.START_TAG) {
					final DeviceFilter deviceFilter = readEntryOne(context, parser);
					if (deviceFilter != null) {
						deviceFilters.add(deviceFilter);
					}
	            }
				eventType = parser.next();
			}
		} catch (final XmlPullParserException e) {
			Log.d(TAG, "XmlPullParserException", e);
		} catch (final IOException e) {
			Log.d(TAG, "IOException", e);
		}

		return Collections.unmodifiableList(deviceFilters);
	}

	/**
	 * read as integer values with default value from xml(w/o exception throws)
	 * resource integer id is also resolved into integer
	 * @param parser
	 * @param namespace
	 * @param name
	 * @param defaultValue
	 * @return
	 */
	private static final int getAttributeInteger(final Context context, final XmlPullParser parser, final String namespace, final String name, final int defaultValue) {
		int result = defaultValue;
		try {
			String v = parser.getAttributeValue(namespace, name);
			if (!TextUtils.isEmpty(v) && v.startsWith("@")) {
				final String r = v.substring(1);
				final int resId = context.getResources().getIdentifier(r, null, context.getPackageName());
				if (resId > 0) {
					result = context.getResources().getInteger(resId);
				}
			} else {
                int radix = 10;
                if (v != null && v.length() > 2 && v.charAt(0) == '0' &&
                    (v.charAt(1) == 'x' || v.charAt(1) == 'X')) {
                    // allow hex values starting with 0x or 0X
                    radix = 16;
                    v = v.substring(2);
                }
				result = Integer.parseInt(v, radix);
			}
		} catch (final NotFoundException e) {
			result = defaultValue;
		} catch (final NumberFormatException e) {
			result = defaultValue;
		} catch (final NullPointerException e) {
			result = defaultValue;
		}
		return result;
	}

	/**
	 * read as boolean values with default value from xml(w/o exception throws)
	 * resource boolean id is also resolved into boolean
	 * if the value is zero, return false, if the value is non-zero integer, return true
	 * @param context
	 * @param parser
	 * @param namespace
	 * @param name
	 * @param defaultValue
	 * @return
	 */
	private static final boolean getAttributeBoolean(final Context context, final XmlPullParser parser, final String namespace, final String name, final boolean defaultValue) {
		boolean result = defaultValue;
		try {
			String v = parser.getAttributeValue(namespace, name);
			if ("TRUE".equalsIgnoreCase(v)) {
				result = true;
			} else if ("FALSE".equalsIgnoreCase(v)) {
				result = false;
			} else if (!TextUtils.isEmpty(v) && v.startsWith("@")) {
				final String r = v.substring(1);
				final int resId = context.getResources().getIdentifier(r, null, context.getPackageName());
				if (resId > 0) {
					result = context.getResources().getBoolean(resId);
				}
			} else {
                int radix = 10;
                if (v != null && v.length() > 2 && v.charAt(0) == '0' &&
                    (v.charAt(1) == 'x' || v.charAt(1) == 'X')) {
                    // allow hex values starting with 0x or 0X
                    radix = 16;
                    v = v.substring(2);
                }
				final int val = Integer.parseInt(v, radix);
				result = val != 0;
			}
		} catch (final NotFoundException e) {
			result = defaultValue;
		} catch (final NumberFormatException e) {
			result = defaultValue;
		} catch (final NullPointerException e) {
			result = defaultValue;
		}
		return result;
	}

	/**
	 * read as String attribute with default value from xml(w/o exception throws)
	 * resource string id is also resolved into string
	 * @param parser
	 * @param namespace
	 * @param name
	 * @param defaultValue
	 * @return
	 */
	private static final String getAttributeString(final Context context, final XmlPullParser parser, final String namespace, final String name, final String defaultValue) {
		String result = defaultValue;
		try {
			result = parser.getAttributeValue(namespace, name);
			if (result == null)
				result = defaultValue;
			if (!TextUtils.isEmpty(result) && result.startsWith("@")) {
				final String r = result.substring(1);
				final int resId = context.getResources().getIdentifier(r, null, context.getPackageName());
				if (resId > 0)
					result = context.getResources().getString(resId);
			}
		} catch (final NotFoundException e) {
			result = defaultValue;
		} catch (final NumberFormatException e) {
			result = defaultValue;
		} catch (final NullPointerException e) {
			result = defaultValue;
		}
		return result;
	}

	public static DeviceFilter readEntryOne(final Context context, final XmlPullParser parser)
			throws XmlPullParserException, IOException {
		int vendorId = -1;
		int productId = -1;
		int deviceClass = -1;
		int deviceSubclass = -1;
		int deviceProtocol = -1;
		boolean exclude = false;
		String manufacturerName = null;
		String productName = null;
		String serialNumber = null;
		boolean hasValue = false;

		String tag;
        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {
        	tag = parser.getName();
        	if (!TextUtils.isEmpty(tag) && (tag.equalsIgnoreCase("usb-device"))) {
        		if (eventType == XmlPullParser.START_TAG) {
        			hasValue = true;
					vendorId = getAttributeInteger(context, parser, null, "vendor-id", -1);
        			if (vendorId == -1) {
        				vendorId = getAttributeInteger(context, parser, null, "vendorId", -1);
        				if (vendorId == -1)
                			vendorId = getAttributeInteger(context, parser, null, "venderId", -1);
        			}
    				productId = getAttributeInteger(context, parser, null, "product-id", -1);
        			if (productId == -1)
            			productId = getAttributeInteger(context, parser, null, "productId", -1);
        			deviceClass = getAttributeInteger(context, parser, null, "class", -1);
        			deviceSubclass = getAttributeInteger(context, parser, null, "subclass", -1);
        			deviceProtocol = getAttributeInteger(context, parser, null, "protocol", -1);
        			manufacturerName = getAttributeString(context, parser, null, "manufacturer-name", null);
        			if (TextUtils.isEmpty(manufacturerName))
        				manufacturerName = getAttributeString(context, parser, null, "manufacture", null);
        			productName = getAttributeString(context, parser, null, "product-name", null);
        			if (TextUtils.isEmpty(productName))
        				productName = getAttributeString(context, parser, null, "product", null);
        			serialNumber = getAttributeString(context, parser, null, "serial-number", null);
        			if (TextUtils.isEmpty(serialNumber))
            			serialNumber = getAttributeString(context, parser, null, "serial", null);
					exclude = getAttributeBoolean(context, parser, null, "exclude", false);
        		} else if (eventType == XmlPullParser.END_TAG) {
        			if (hasValue) {
	        			return new DeviceFilter(vendorId, productId, deviceClass,
	        					deviceSubclass, deviceProtocol, manufacturerName, productName,
	        					serialNumber, exclude);
        			}
        		}
        	}
        	eventType = parser.next();
        }
        return null;
	}

/*	public void write(XmlSerializer serializer) throws IOException {
		serializer.startTag(null, "usb-device");
		if (mVendorId != -1) {
			serializer
					.attribute(null, "vendor-id", Integer.toString(mVendorId));
		}
		if (mProductId != -1) {
			serializer.attribute(null, "product-id",
					Integer.toString(mProductId));
		}
		if (mClass != -1) {
			serializer.attribute(null, "class", Integer.toString(mClass));
		}
		if (mSubclass != -1) {
			serializer.attribute(null, "subclass", Integer.toString(mSubclass));
		}
		if (mProtocol != -1) {
			serializer.attribute(null, "protocol", Integer.toString(mProtocol));
		}
		if (mManufacturerName != null) {
			serializer.attribute(null, "manufacturer-name", mManufacturerName);
		}
		if (mProductName != null) {
			serializer.attribute(null, "product-name", mProductName);
		}
		if (mSerialNumber != null) {
			serializer.attribute(null, "serial-number", mSerialNumber);
		}
		serializer.attribute(null, "serial-number", Boolean.toString(isExclude));
		serializer.endTag(null, "usb-device");
	} */

	/**
	 * 指定したクラス・サブクラス・プロトコルがこのDeviceFilterとマッチするかどうかを返す
	 * mExcludeフラグは別途#isExcludeか自前でチェックすること
	 * @param clasz
	 * @param subclass
	 * @param protocol
	 * @return
	 */
	private boolean matches(final int clasz, final int subclass, final int protocol) {
		return ((mClass == -1 || clasz == mClass)
				&& (mSubclass == -1 || subclass == mSubclass) && (mProtocol == -1 || protocol == mProtocol));
	}

	/**
	 * 指定したUsbDeviceがこのDeviceFilterにマッチするかどうかを返す
	 * mExcludeフラグは別途#isExcludeか自前でチェックすること
	 * @param device
	 * @return
	 */
	public boolean matches(final UsbDevice device) {
		if (mVendorId != -1 && device.getVendorId() != mVendorId) {
			return false;
		}
		if (mProductId != -1 && device.getProductId() != mProductId) {
			return false;
		}
/*		if (mManufacturerName != null && device.getManufacturerName() == null)
			return false;
		if (mProductName != null && device.getProductName() == null)
			return false;
		if (mSerialNumber != null && device.getSerialNumber() == null)
			return false;
		if (mManufacturerName != null && device.getManufacturerName() != null
				&& !mManufacturerName.equals(device.getManufacturerName()))
			return false;
		if (mProductName != null && device.getProductName() != null
				&& !mProductName.equals(device.getProductName()))
			return false;
		if (mSerialNumber != null && device.getSerialNumber() != null
				&& !mSerialNumber.equals(device.getSerialNumber()))
			return false; */

		// check device class/subclass/protocol
		if (matches(device.getDeviceClass(), device.getDeviceSubclass(), device.getDeviceProtocol())) {
			return true;
		}

		// if device doesn't match, check the interfaces
		final int count = device.getInterfaceCount();
		for (int i = 0; i < count; i++) {
			final UsbInterface intf = device.getInterface(i);
			if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(), intf.getInterfaceProtocol())) {
				return true;
			}
		}

		return false;
	}

	/**
	 * このDeviceFilterに一致してかつmExcludeがtrueならtrueを返す
	 * @param device
	 * @return
	 */
	public boolean isExclude(final UsbDevice device) {
		return isExclude && matches(device);
	}

	/**
	 * これって要らんかも, equalsでできる気が
	 * @param f
	 * @return
	 */
	public boolean matches(final DeviceFilter f) {
		if (isExclude != f.isExclude) {
			return false;
		}
		if (mVendorId != -1 && f.mVendorId != mVendorId) {
			return false;
		}
		if (mProductId != -1 && f.mProductId != mProductId) {
			return false;
		}
		if (f.mManufacturerName != null && mManufacturerName == null) {
			return false;
		}
		if (f.mProductName != null && mProductName == null) {
			return false;
		}
		if (f.mSerialNumber != null && mSerialNumber == null) {
			return false;
		}
		if (mManufacturerName != null && f.mManufacturerName != null
				&& !mManufacturerName.equals(f.mManufacturerName)) {
			return false;
		}
		if (mProductName != null && f.mProductName != null
				&& !mProductName.equals(f.mProductName)) {
			return false;
		}
		if (mSerialNumber != null && f.mSerialNumber != null
				&& !mSerialNumber.equals(f.mSerialNumber)) {
			return false;
		}

		// check device class/subclass/protocol
		return matches(f.mClass, f.mSubclass, f.mProtocol);
	}

	@Override
	public boolean equals(final Object obj) {
		// can't compare if we have wildcard strings
		if (mVendorId == -1 || mProductId == -1 || mClass == -1
				|| mSubclass == -1 || mProtocol == -1) {
			return false;
		}
		if (obj instanceof DeviceFilter) {
			final DeviceFilter filter = (DeviceFilter) obj;

			if (filter.mVendorId != mVendorId
					|| filter.mProductId != mProductId
					|| filter.mClass != mClass || filter.mSubclass != mSubclass
					|| filter.mProtocol != mProtocol) {
				return false;
			}
			if ((filter.mManufacturerName != null && mManufacturerName == null)
					|| (filter.mManufacturerName == null && mManufacturerName != null)
					|| (filter.mProductName != null && mProductName == null)
					|| (filter.mProductName == null && mProductName != null)
					|| (filter.mSerialNumber != null && mSerialNumber == null)
					|| (filter.mSerialNumber == null && mSerialNumber != null)) {
				return false;
			}
			if ((filter.mManufacturerName != null && mManufacturerName != null && !mManufacturerName
					.equals(filter.mManufacturerName))
					|| (filter.mProductName != null && mProductName != null && !mProductName
							.equals(filter.mProductName))
					|| (filter.mSerialNumber != null && mSerialNumber != null && !mSerialNumber
							.equals(filter.mSerialNumber))) {
				return false;
			}
			return (filter.isExclude != isExclude);
		}
		if (obj instanceof UsbDevice) {
			final UsbDevice device = (UsbDevice) obj;
			if (isExclude
					|| (device.getVendorId() != mVendorId)
					|| (device.getProductId() != mProductId)
					|| (device.getDeviceClass() != mClass)
					|| (device.getDeviceSubclass() != mSubclass)
					|| (device.getDeviceProtocol() != mProtocol) ) {
				return false;
			}
/*			if ((mManufacturerName != null && device.getManufacturerName() == null)
					|| (mManufacturerName == null && device
							.getManufacturerName() != null)
					|| (mProductName != null && device.getProductName() == null)
					|| (mProductName == null && device.getProductName() != null)
					|| (mSerialNumber != null && device.getSerialNumber() == null)
					|| (mSerialNumber == null && device.getSerialNumber() != null)) {
				return (false);
			} */
/*			if ((device.getManufacturerName() != null && !mManufacturerName
					.equals(device.getManufacturerName()))
					|| (device.getProductName() != null && !mProductName
							.equals(device.getProductName()))
					|| (device.getSerialNumber() != null && !mSerialNumber
							.equals(device.getSerialNumber()))) {
				return (false);
			} */
			return true;
		}
		return false;
	}

	@Override
	public int hashCode() {
		return (((mVendorId << 16) | mProductId) ^ ((mClass << 16)
				| (mSubclass << 8) | mProtocol));
	}

	@Override
	public String toString() {
		return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId="
			+ mProductId + ",mClass=" + mClass + ",mSubclass=" + mSubclass
			+ ",mProtocol=" + mProtocol
			+ ",mManufacturerName=" + mManufacturerName
			+ ",mProductName=" + mProductName
			+ ",mSerialNumber=" + mSerialNumber
			+ ",isExclude=" + isExclude
			+ "]";
	}

}
