/**
 * Radius Networks, Inc.
 * http://www.radiusnetworks.com
 *
 * @author David G. Young
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.altbeacon.beacon;

import android.os.Parcel;
import android.os.Parcelable;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * This class represents a criteria of fields used to match beacons.
 *
 * The uniqueId field is used to distinguish this Region in the system.  When you set up
 * monitoring or ranging based on a Region and later want to stop monitoring or ranging,
 * you must do so by passing a Region object that has the same uniqueId field value.  If it
 * doesn't match, you can't cancel the operation.  There is no other purpose to this field.
 *
 * The region can be constructed from a multi-part identifier.  The first identifier is the most
 * significant, the second the second most significant, etc.
 *
 * When constructing a range, any or all of these identifiers may be set to null,
 * which indicates that they are a wildcard and will match any value.
 *
 * @author dyoung
 *
 */
public class Region implements Parcelable, Serializable {
    private static final String TAG = "Region";
    private static final Pattern MAC_PATTERN = Pattern.compile("^[0-9A-Fa-f]{2}\\:[0-9A-Fa-f]{2}\\:[0-9A-Fa-f]{2}\\:[0-9A-Fa-f]{2}\\:[0-9A-Fa-f]{2}\\:[0-9A-Fa-f]{2}$");
    /**
     * Required to make class Parcelable
     */
    public static final Parcelable.Creator<Region> CREATOR
            = new Parcelable.Creator<Region>() {
        public Region createFromParcel(Parcel in) {
            return new Region(in);
        }

        public Region[] newArray(int size) {
            return new Region[size];
        }
    };
    protected final List<Identifier> mIdentifiers;
    protected final String mBluetoothAddress;
    protected final String mUniqueId;

    /**
     * Constructs a new Region object to be used for Ranging or Monitoring
     * @param uniqueId - A unique identifier used to later cancel Ranging and Monitoring, or change the region being Ranged/Monitored
     * @param id1 - most significant identifier (can be null)
     * @param id2 - second most significant identifier (can be null)
     * @param id3 - third most significant identifier (can be null)
     */
    public Region(String uniqueId, Identifier id1, Identifier id2, Identifier id3) {
        this.mIdentifiers = new ArrayList<Identifier>(3);
        this.mIdentifiers.add(id1);
        this.mIdentifiers.add(id2);
        this.mIdentifiers.add(id3);
        this.mUniqueId = uniqueId;
        this.mBluetoothAddress = null;
        if (uniqueId == null) {
            throw new NullPointerException("uniqueId may not be null");
        }
    }

    /**
     * Constructs a new Region object to be used for Ranging or Monitoring
     * @param uniqueId - A unique identifier used to later cancel Ranging and Monitoring, or change the region being Ranged/Monitored
     * @param identifiers - list of identifiers for this region
     */
    public Region(String uniqueId, List<Identifier> identifiers) {
       this(uniqueId, identifiers, null);
    }

    /**
     * Constructs a new Region object to be used for Ranging or Monitoring
     * @param uniqueId - A unique identifier used to later cancel Ranging and Monitoring, or change the region being Ranged/Monitored
     * @param identifiers - list of identifiers for this region
     * @param bluetoothAddress - mac address
     */
    public Region(String uniqueId, List<Identifier> identifiers, String bluetoothAddress) {
        validateMac(bluetoothAddress);
        this.mIdentifiers = new ArrayList<Identifier>(identifiers);
        this.mUniqueId = uniqueId;
        this.mBluetoothAddress = bluetoothAddress;
        if (uniqueId == null) {
            throw new NullPointerException("uniqueId may not be null");
        }
    }

    /**
     * Constructs a new Region object to be used for Ranging or Monitoring
     * @param uniqueId - A unique identifier used to later cancel Ranging and Monitoring, or change the region being Ranged/Monitored
     * @param bluetoothAddress - mac address used to match beacons
     */
    public Region(String uniqueId, String bluetoothAddress) {
        validateMac(bluetoothAddress);
        this.mBluetoothAddress = bluetoothAddress;
        this.mUniqueId = uniqueId;
        this.mIdentifiers = new ArrayList<Identifier>();
        if (uniqueId == null) {
            throw new NullPointerException("uniqueId may not be null");
        }
    }

    /**
     * Convenience method to get the first identifier
     * @return
     */
    public Identifier getId1() {
        return getIdentifier(0);
    }

    /**
     * Convenience method to get the second identifier
     * @return
     */
    public Identifier getId2() {
        return getIdentifier(1);
    }

    /**
     * Convenience method to get the third identifier
     * @return
     */
    public Identifier getId3() {
        return getIdentifier(2);
    }

    /**
     * Returns the 0-indexed identifier
     * Note:  IMPORTANT:  to get id1, you would call getIdentifier(0);
     * @param i
     * @return
     */
    public Identifier getIdentifier(int i) {
        return mIdentifiers.size() > i ? mIdentifiers.get(i) : null;
    }

    public List<Identifier> getIdentifiers() {
        return new ArrayList<>(mIdentifiers);
    }

    /**
     * Returns the identifier used to start or stop ranging/monitoring this region when calling
     * the <code>BeaconManager</code> methods.
     * @return
     */
    public String getUniqueId() {
        return mUniqueId;
    }

    /**
     * Returns the mac address used to filter for beacons
     */
    public String getBluetoothAddress() { return mBluetoothAddress; }

    /**
     * Checks to see if an Beacon object is included in the matching criteria of this Region
     * @param beacon the beacon to check to see if it is in the Region
     * @return true if is covered
     */
    public boolean matchesBeacon(Beacon beacon) {
        // All identifiers must match, or the corresponding region identifier must be null.
        for (int i = mIdentifiers.size(); --i >= 0; ) {
            final Identifier identifier = mIdentifiers.get(i);
            Identifier beaconIdentifier = null;
            if (i < beacon.mIdentifiers.size()) {
                beaconIdentifier = beacon.getIdentifier(i);
            }
            if ((beaconIdentifier == null && identifier != null) ||
                    (beaconIdentifier != null  && identifier != null && !identifier.equals(beaconIdentifier))) {
                return false;
            }
        }
        if (mBluetoothAddress != null && !mBluetoothAddress.equalsIgnoreCase(beacon.mBluetoothAddress)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        return this.mUniqueId.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        if (other instanceof Region) {
            return ((Region)other).mUniqueId.equals(this.mUniqueId);
        }
        return false;
    }

    public boolean hasSameIdentifiers(Region region) {
        if (region.mIdentifiers.size() == this.mIdentifiers.size()) {
            for (int i = 0 ; i < region.mIdentifiers.size(); i++) {

                if (region.getIdentifier(i) == null && this.getIdentifier(i) != null) {
                    return false;
                }
                else if (region.getIdentifier(i) != null && this.getIdentifier(i) == null) {
                    return false;
                }
                else if (!(region.getIdentifier(i) == null && this.getIdentifier(i) == null)) {
                    if (!region.getIdentifier(i).equals(this.getIdentifier(i))) {
                        return false;
                    }
                }
            }
        }
        else {
            return false;
        }
        return true;
    }



    public String toString() {
        StringBuilder sb = new StringBuilder();
        int i = 1;
        for (Identifier identifier: mIdentifiers) {
            if (i > 1) {
                sb.append(" ");
            }
            sb.append("id");
            sb.append(i);
            sb.append(": ");
            sb.append(identifier == null ? "null" : identifier.toString());
            i++;
        }
        return sb.toString();
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeString(mUniqueId);
        out.writeString(mBluetoothAddress);
        out.writeInt(mIdentifiers.size());

        for (Identifier identifier: mIdentifiers) {
            if (identifier != null) {
                out.writeString(identifier.toString());
            }
            else {
                out.writeString(null);
            }
        }
    }


    protected Region(Parcel in) {
        mUniqueId = in.readString();
        mBluetoothAddress = in.readString();
        int size = in.readInt();
        mIdentifiers = new ArrayList<Identifier>(size);
        for (int i = 0; i < size; i++) {
            String identifierString = in.readString();
            if (identifierString == null) {
                mIdentifiers.add(null);
            } else {
                Identifier identifier = Identifier.parse(identifierString);
                mIdentifiers.add(identifier);
            }
        }
    }

    private void validateMac(String mac) throws IllegalArgumentException {
        if (mac != null) {
            if(!MAC_PATTERN.matcher(mac).matches()) {
                throw new IllegalArgumentException("Invalid mac address: '"+mac+"' Must be 6 hex bytes separated by colons.");
            }
        }
    }

    /**
     * Returns a clone of this instance.
     * @deprecated instances of this class are immutable and therefore don't have to be cloned when
     * used in concurrent code.
     * @return a new instance of this class with the same uniqueId and identifiers
     */
    @Override
    @Deprecated
    public Region clone() {
        return new Region(mUniqueId, mIdentifiers, mBluetoothAddress);
    }

}
