// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.util.*;

/**
 * A DNS Zone.  This encapsulates all data related to a Zone, and provides
 * convenient lookup methods.
 *
 * @author Brian Wellington
 */

public class Zone implements Serializable {

    private static final long serialVersionUID = -9220510891189510942L;

    /**
     * A primary zone
     */
    public static final int PRIMARY = 1;

    /**
     * A secondary zone
     */
    public static final int SECONDARY = 2;

    private Map data;
    private Name origin;
    private Object originNode;
    private int dclass = DClass.IN;
    private RRset NS;
    private SOARecord SOA;
    private boolean hasWild;

    class ZoneIterator implements Iterator {
        private Iterator zentries;
        private RRset[] current;
        private int count;
        private boolean wantLastSOA;

        ZoneIterator(boolean axfr) {
            synchronized (Zone.this) {
                zentries = data.entrySet().iterator();
            }
            wantLastSOA = axfr;
            RRset[] sets = allRRsets(originNode);
            current = new RRset[sets.length];
            for (int i = 0, j = 2; i < sets.length; i++) {
                int type = sets[i].getType();
                if (type == Type.SOA)
                    current[0] = sets[i];
                else if (type == Type.NS)
                    current[1] = sets[i];
                else
                    current[j++] = sets[i];
            }
        }

        public boolean
        hasNext() {
            return (current != null || wantLastSOA);
        }

        public Object
        next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            if (current == null) {
                wantLastSOA = false;
                return oneRRset(originNode, Type.SOA);
            }
            Object set = current[count++];
            if (count == current.length) {
                current = null;
                while (zentries.hasNext()) {
                    Map.Entry entry = (Map.Entry) zentries.next();
                    if (entry.getKey().equals(origin))
                        continue;
                    RRset[] sets = allRRsets(entry.getValue());
                    if (sets.length == 0)
                        continue;
                    current = sets;
                    count = 0;
                    break;
                }
            }
            return set;
        }

        public void
        remove() {
            throw new UnsupportedOperationException();
        }
    }

    private void
    validate() throws IOException {
        originNode = exactName(origin);
        if (originNode == null)
            throw new IOException(origin + ": no data specified");

        RRset rrset = oneRRset(originNode, Type.SOA);
        if (rrset == null || rrset.size() != 1)
            throw new IOException(origin +
                    ": exactly 1 SOA must be specified");
        Iterator it = rrset.rrs();
        SOA = (SOARecord) it.next();

        NS = oneRRset(originNode, Type.NS);
        if (NS == null)
            throw new IOException(origin + ": no NS set specified");
    }

    private final void
    maybeAddRecord(Record record) throws IOException {
        int rtype = record.getType();
        Name name = record.getName();

        if (rtype == Type.SOA && !name.equals(origin)) {
            throw new IOException("SOA owner " + name +
                    " does not match zone origin " +
                    origin);
        }
        if (name.subdomain(origin))
            addRecord(record);
    }

    /**
     * Creates a Zone from the records in the specified master file.
     *
     * @param zone The name of the zone.
     * @param file The master file to read from.
     * @see Master
     */
    public Zone(Name zone, String file) throws IOException {
        data = new TreeMap();

        if (zone == null)
            throw new IllegalArgumentException("no zone name specified");
        Master m = new Master(file, zone);
        Record record;

        origin = zone;
        while ((record = m.nextRecord()) != null)
            maybeAddRecord(record);
        validate();
    }

    /**
     * Creates a Zone from an array of records.
     *
     * @param zone    The name of the zone.
     * @param records The records to add to the zone.
     * @see Master
     */
    public Zone(Name zone, Record[] records) throws IOException {
        data = new TreeMap();

        if (zone == null)
            throw new IllegalArgumentException("no zone name specified");
        origin = zone;
        for (int i = 0; i < records.length; i++)
            maybeAddRecord(records[i]);
        validate();
    }

    private void
    fromXFR(ZoneTransferIn xfrin) throws IOException, ZoneTransferException {
        data = new TreeMap();

        origin = xfrin.getName();
        List records = xfrin.run();
        for (Iterator it = records.iterator(); it.hasNext(); ) {
            Record record = (Record) it.next();
            maybeAddRecord(record);
        }
        if (!xfrin.isAXFR())
            throw new IllegalArgumentException("zones can only be " +
                    "created from AXFRs");
        validate();
    }

    /**
     * Creates a Zone by doing the specified zone transfer.
     *
     * @param xfrin The incoming zone transfer to execute.
     * @see ZoneTransferIn
     */
    public Zone(ZoneTransferIn xfrin) throws IOException, ZoneTransferException {
        fromXFR(xfrin);
    }

    /**
     * Creates a Zone by performing a zone transfer to the specified host.
     *
     * @see ZoneTransferIn
     */
    public Zone(Name zone, int dclass, String remote)
            throws IOException, ZoneTransferException {
        ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(zone, remote, null);
        xfrin.setDClass(dclass);
        fromXFR(xfrin);
    }

    /**
     * Returns the Zone's origin
     */
    public Name
    getOrigin() {
        return origin;
    }

    /**
     * Returns the Zone origin's NS records
     */
    public RRset
    getNS() {
        return NS;
    }

    /**
     * Returns the Zone's SOA record
     */
    public SOARecord
    getSOA() {
        return SOA;
    }

    /**
     * Returns the Zone's class
     */
    public int
    getDClass() {
        return dclass;
    }

    private synchronized Object
    exactName(Name name) {
        return data.get(name);
    }

    private synchronized RRset[]
    allRRsets(Object types) {
        if (types instanceof List) {
            List typelist = (List) types;
            return (RRset[]) typelist.toArray(new RRset[typelist.size()]);
        } else {
            RRset set = (RRset) types;
            return new RRset[]{set};
        }
    }

    private synchronized RRset
    oneRRset(Object types, int type) {
        if (type == Type.ANY)
            throw new IllegalArgumentException("oneRRset(ANY)");
        if (types instanceof List) {
            List list = (List) types;
            for (int i = 0; i < list.size(); i++) {
                RRset set = (RRset) list.get(i);
                if (set.getType() == type)
                    return set;
            }
        } else {
            RRset set = (RRset) types;
            if (set.getType() == type)
                return set;
        }
        return null;
    }

    private synchronized RRset
    findRRset(Name name, int type) {
        Object types = exactName(name);
        if (types == null)
            return null;
        return oneRRset(types, type);
    }

    private synchronized void
    addRRset(Name name, RRset rrset) {
        if (!hasWild && name.isWild())
            hasWild = true;
        Object types = data.get(name);
        if (types == null) {
            data.put(name, rrset);
            return;
        }
        int rtype = rrset.getType();
        if (types instanceof List) {
            List list = (List) types;
            for (int i = 0; i < list.size(); i++) {
                RRset set = (RRset) list.get(i);
                if (set.getType() == rtype) {
                    list.set(i, rrset);
                    return;
                }
            }
            list.add(rrset);
        } else {
            RRset set = (RRset) types;
            if (set.getType() == rtype)
                data.put(name, rrset);
            else {
                LinkedList list = new LinkedList();
                list.add(set);
                list.add(rrset);
                data.put(name, list);
            }
        }
    }

    private synchronized void
    removeRRset(Name name, int type) {
        Object types = data.get(name);
        if (types == null) {
            return;
        }
        if (types instanceof List) {
            List list = (List) types;
            for (int i = 0; i < list.size(); i++) {
                RRset set = (RRset) list.get(i);
                if (set.getType() == type) {
                    list.remove(i);
                    if (list.size() == 0)
                        data.remove(name);
                    return;
                }
            }
        } else {
            RRset set = (RRset) types;
            if (set.getType() != type)
                return;
            data.remove(name);
        }
    }

    private synchronized SetResponse
    lookup(Name name, int type) {
        int labels;
        int olabels;
        int tlabels;
        RRset rrset;
        Name tname;
        Object types;
        SetResponse sr;

        if (!name.subdomain(origin))
            return SetResponse.ofType(SetResponse.NXDOMAIN);

        labels = name.labels();
        olabels = origin.labels();

        for (tlabels = olabels; tlabels <= labels; tlabels++) {
            boolean isOrigin = (tlabels == olabels);
            boolean isExact = (tlabels == labels);

            if (isOrigin)
                tname = origin;
            else if (isExact)
                tname = name;
            else
                tname = new Name(name, labels - tlabels);

            types = exactName(tname);
            if (types == null)
                continue;

		/* If this is a delegation, return that. */
            if (!isOrigin) {
                RRset ns = oneRRset(types, Type.NS);
                if (ns != null)
                    return new SetResponse(SetResponse.DELEGATION,
                            ns);
            }

		/* If this is an ANY lookup, return everything. */
            if (isExact && type == Type.ANY) {
                sr = new SetResponse(SetResponse.SUCCESSFUL);
                RRset[] sets = allRRsets(types);
                for (int i = 0; i < sets.length; i++)
                    sr.addRRset(sets[i]);
                return sr;
            }

		/*
         * If this is the name, look for the actual type or a CNAME.
		 * Otherwise, look for a DNAME.
		 */
            if (isExact) {
                rrset = oneRRset(types, type);
                if (rrset != null) {
                    sr = new SetResponse(SetResponse.SUCCESSFUL);
                    sr.addRRset(rrset);
                    return sr;
                }
                rrset = oneRRset(types, Type.CNAME);
                if (rrset != null)
                    return new SetResponse(SetResponse.CNAME,
                            rrset);
            } else {
                rrset = oneRRset(types, Type.DNAME);
                if (rrset != null)
                    return new SetResponse(SetResponse.DNAME,
                            rrset);
            }

		/* We found the name, but not the type. */
            if (isExact)
                return SetResponse.ofType(SetResponse.NXRRSET);
        }

        if (hasWild) {
            for (int i = 0; i < labels - olabels; i++) {
                tname = name.wild(i + 1);

                types = exactName(tname);
                if (types == null)
                    continue;

                rrset = oneRRset(types, type);
                if (rrset != null) {
                    sr = new SetResponse(SetResponse.SUCCESSFUL);
                    sr.addRRset(rrset);
                    return sr;
                }
            }
        }

        return SetResponse.ofType(SetResponse.NXDOMAIN);
    }

    /**
     * Looks up Records in the Zone.  This follows CNAMEs and wildcards.
     *
     * @param name The name to look up
     * @param type The type to look up
     * @return A SetResponse object
     * @see SetResponse
     */
    public SetResponse
    findRecords(Name name, int type) {
        return lookup(name, type);
    }

    /**
     * Looks up Records in the zone, finding exact matches only.
     *
     * @param name The name to look up
     * @param type The type to look up
     * @return The matching RRset
     * @see RRset
     */
    public RRset
    findExactMatch(Name name, int type) {
        Object types = exactName(name);
        if (types == null)
            return null;
        return oneRRset(types, type);
    }

    /**
     * Adds an RRset to the Zone
     *
     * @param rrset The RRset to be added
     * @see RRset
     */
    public void
    addRRset(RRset rrset) {
        Name name = rrset.getName();
        addRRset(name, rrset);
    }

    /**
     * Adds a Record to the Zone
     *
     * @param r The record to be added
     * @see Record
     */
    public void
    addRecord(Record r) {
        Name name = r.getName();
        int rtype = r.getRRsetType();
        synchronized (this) {
            RRset rrset = findRRset(name, rtype);
            if (rrset == null) {
                rrset = new RRset(r);
                addRRset(name, rrset);
            } else {
                rrset.addRR(r);
            }
        }
    }

    /**
     * Removes a record from the Zone
     *
     * @param r The record to be removed
     * @see Record
     */
    public void
    removeRecord(Record r) {
        Name name = r.getName();
        int rtype = r.getRRsetType();
        synchronized (this) {
            RRset rrset = findRRset(name, rtype);
            if (rrset == null)
                return;
            if (rrset.size() == 1 && rrset.first().equals(r))
                removeRRset(name, rtype);
            else
                rrset.deleteRR(r);
        }
    }

    /**
     * Returns an Iterator over the RRsets in the zone.
     */
    public Iterator
    iterator() {
        return new ZoneIterator(false);
    }

    /**
     * Returns an Iterator over the RRsets in the zone that can be used to
     * construct an AXFR response.  This is identical to {@link #iterator} except
     * that the SOA is returned at the end as well as the beginning.
     */
    public Iterator
    AXFR() {
        return new ZoneIterator(true);
    }

    private void
    nodeToString(StringBuffer sb, Object node) {
        RRset[] sets = allRRsets(node);
        for (int i = 0; i < sets.length; i++) {
            RRset rrset = sets[i];
            Iterator it = rrset.rrs();
            while (it.hasNext())
                sb.append(it.next() + "\n");
            it = rrset.sigs();
            while (it.hasNext())
                sb.append(it.next() + "\n");
        }
    }

    /**
     * Returns the contents of the Zone in master file format.
     */
    public synchronized String
    toMasterFile() {
        Iterator zentries = data.entrySet().iterator();
        StringBuffer sb = new StringBuffer();
        nodeToString(sb, originNode);
        while (zentries.hasNext()) {
            Map.Entry entry = (Map.Entry) zentries.next();
            if (!origin.equals(entry.getKey()))
                nodeToString(sb, entry.getValue());
        }
        return sb.toString();
    }

    /**
     * Returns the contents of the Zone as a string (in master file format).
     */
    public String
    toString() {
        return toMasterFile();
    }

}
