/*
 * 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.apache.zookeeper;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.jute.Record;
import org.apache.zookeeper.common.PathUtils;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.proto.CheckVersionRequest;
import org.apache.zookeeper.proto.CreateRequest;
import org.apache.zookeeper.proto.CreateTTLRequest;
import org.apache.zookeeper.proto.DeleteRequest;
import org.apache.zookeeper.proto.GetChildrenRequest;
import org.apache.zookeeper.proto.GetDataRequest;
import org.apache.zookeeper.proto.SetDataRequest;
import org.apache.zookeeper.server.EphemeralType;

/**
 * Represents a single operation in a multi-operation transaction.  Each operation can be a create, update,
 * delete, a version check or just read operations like getChildren or getData.
 *
 * Sub-classes of Op each represent each detailed type but should not normally be referenced except via
 * the provided factory methods.
 *
 * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode)
 * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode, org.apache.zookeeper.AsyncCallback.StringCallback, Object)
 * @see ZooKeeper#delete(String, int)
 * @see ZooKeeper#setData(String, byte[], int)
 * @see ZooKeeper#getData(String, boolean, Stat)
 * @see ZooKeeper#getChildren(String, boolean)
 */
public abstract class Op {

    public enum OpKind {
        TRANSACTION,
        READ
    }

    private int type;
    private String path;
    private OpKind opKind;

    // prevent untyped construction
    private Op(int type, String path, OpKind opKind) {
        this.type = type;
        this.path = path;
        this.opKind = opKind;
    }

    /**
     * Constructs a create operation.  Arguments are as for the ZooKeeper method of the same name.
     * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode)
     * @see CreateMode#fromFlag(int)
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param acl
     *                the acl for the node
     * @param flags
     *                specifying whether the node to be created is ephemeral
     *                and/or sequential but using the integer encoding.
     */
    public static Op create(String path, byte[] data, List<ACL> acl, int flags) {
        return new Create(path, data, acl, flags);
    }

    /**
     * Constructs a create operation.  Arguments are as for the ZooKeeper method of the same name
     * but adding an optional ttl
     * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode)
     * @see CreateMode#fromFlag(int)
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param acl
     *                the acl for the node
     * @param flags
     *                specifying whether the node to be created is ephemeral
     *                and/or sequential but using the integer encoding.
     * @param ttl
     *                optional ttl or 0 (flags must imply a TTL creation mode)
     */
    public static Op create(String path, byte[] data, List<ACL> acl, int flags, long ttl) {
        CreateMode createMode = CreateMode.fromFlag(flags, CreateMode.PERSISTENT);
        if (createMode.isTTL()) {
            return new CreateTTL(path, data, acl, createMode, ttl);
        }
        return new Create(path, data, acl, flags);
    }

    /**
     * Constructs a create operation.  Arguments are as for the ZooKeeper method of the same name.
     * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode)
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param acl
     *                the acl for the node
     * @param createMode
     *                specifying whether the node to be created is ephemeral
     *                and/or sequential
     */
    public static Op create(String path, byte[] data, List<ACL> acl, CreateMode createMode) {
        return new Create(path, data, acl, createMode);
    }

    /**
     * Constructs a create operation.  Arguments are as for the ZooKeeper method of the same name
     * but adding an optional ttl
     * @see ZooKeeper#create(String, byte[], java.util.List, CreateMode)
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param acl
     *                the acl for the node
     * @param createMode
     *                specifying whether the node to be created is ephemeral
     *                and/or sequential
     * @param ttl
     *                optional ttl or 0 (createMode must imply a TTL)
     */
    public static Op create(String path, byte[] data, List<ACL> acl, CreateMode createMode, long ttl) {
        if (createMode.isTTL()) {
            return new CreateTTL(path, data, acl, createMode, ttl);
        }
        return new Create(path, data, acl, createMode);
    }

    /**
     * Constructs a create operation which uses given op code if no one is inferred from create mode.
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param options
     *                options for creating znode
     * @param defaultOpCode
     *                op code to be used if no one is inferred from create mode
     */
    static Op create(String path, byte[] data, CreateOptions options, int defaultOpCode) {
        if (options.getCreateMode().isTTL()) {
            return new CreateTTL(path, data, options.getAcl(), options.getCreateMode(), options.getTtl());
        }
        return new Create(path, data, options.getAcl(), options.getCreateMode(), defaultOpCode);
    }

    /**
     * Constructs a create operation which uses {@link ZooDefs.OpCode#create2} if no one is inferred from create mode.
     *
     * <p>The corresponding {@link OpResult.CreateResult#getStat()} could be null if connected to server without this
     * patch.
     *
     * @param path
     *                the path for the node
     * @param data
     *                the initial data for the node
     * @param options
     *                options for creating znode
     */
    public static Op create(String path, byte[] data, CreateOptions options) {
        return create(path, data, options, ZooDefs.OpCode.create2);
    }

    /**
     * Constructs a delete operation.  Arguments are as for the ZooKeeper method of the same name.
     * @see ZooKeeper#delete(String, int)
     *
     * @param path
     *                the path of the node to be deleted.
     * @param version
     *                the expected node version.
     */
    public static Op delete(String path, int version) {
        return new Delete(path, version);
    }

    /**
     * Constructs an update operation.  Arguments are as for the ZooKeeper method of the same name.
     * @see ZooKeeper#setData(String, byte[], int)
     *
     * @param path
     *                the path of the node
     * @param data
     *                the data to set
     * @param version
     *                the expected matching version
     */
    public static Op setData(String path, byte[] data, int version) {
        return new SetData(path, data, version);
    }

    /**
     * Constructs an version check operation.  Arguments are as for the ZooKeeper.setData method except that
     * no data is provided since no update is intended.  The purpose for this is to allow read-modify-write
     * operations that apply to multiple znodes, but where some of the znodes are involved only in the read,
     * not the write.  A similar effect could be achieved by writing the same data back, but that leads to
     * way more version updates than are necessary and more writing in general.
     *
     * @param path
     *                the path of the node
     * @param version
     *                the expected matching version
     */
    public static Op check(String path, int version) {
        return new Check(path, version);
    }

    public static Op getChildren(String path) {
        return new GetChildren(path);
    }

    public static Op getData(String path) {
        return new GetData(path);
    }

    /**
     * Gets the integer type code for an Op.  This code should be as from ZooDefs.OpCode
     * @see ZooDefs.OpCode
     * @return The type code.
     */
    public int getType() {
        return type;
    }

    /**
     * Gets the path for an Op.
     * @return The path.
     */
    public String getPath() {
        return path;
    }

    /**
     * Gets the kind of an Op.
     * @return The OpKind value.
     */
    public OpKind getKind() {
        return opKind;
    }

    /**
     * Encodes an op for wire transmission.
     * @return An appropriate Record structure.
     */
    public abstract Record toRequestRecord();

    /**
     * Reconstructs the transaction with the chroot prefix.
     * @return transaction with chroot.
     */
    abstract Op withChroot(String addRootPrefix);

    /**
     * Performs client path validations.
     *
     * @throws IllegalArgumentException
     *             if an invalid path is specified
     * @throws KeeperException.BadArgumentsException
     *             if an invalid create mode flag is specified
     */
    void validate() throws KeeperException {
        PathUtils.validatePath(path);
    }

    //////////////////
    // these internal classes are public, but should not generally be referenced.
    //
    public static class Create extends Op {

        protected byte[] data;
        protected List<ACL> acl;
        protected int flags;

        private Create(String path, byte[] data, List<ACL> acl, int flags) {
            this(path, data, acl, flags, ZooDefs.OpCode.create);
        }

        private Create(String path, byte[] data, List<ACL> acl, int flags, int defaultOpCode) {
            super(getOpcode(CreateMode.fromFlag(flags, CreateMode.PERSISTENT), defaultOpCode), path, OpKind.TRANSACTION);
            this.data = data;
            this.acl = acl;
            this.flags = flags;
        }

        private static int getOpcode(CreateMode createMode, int defaultOpCode) {
            if (createMode.isTTL()) {
                return ZooDefs.OpCode.createTTL;
            }
            return createMode.isContainer() ? ZooDefs.OpCode.createContainer : defaultOpCode;
        }

        private Create(String path, byte[] data, List<ACL> acl, CreateMode createMode) {
            this(path, data, acl, createMode, ZooDefs.OpCode.create);
        }

        private Create(String path, byte[] data, List<ACL> acl, CreateMode createMode, int defaultOpCode) {
            super(getOpcode(createMode, defaultOpCode), path, OpKind.TRANSACTION);
            this.data = data;
            this.acl = acl;
            this.flags = createMode.toFlag();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof Create)) {
                return false;
            }

            Create op = (Create) o;

            boolean aclEquals = true;
            Iterator<ACL> i = op.acl.iterator();
            for (ACL acl : op.acl) {
                boolean hasMoreData = i.hasNext();
                if (!hasMoreData) {
                    aclEquals = false;
                    break;
                }
                ACL otherAcl = i.next();
                if (!acl.equals(otherAcl)) {
                    aclEquals = false;
                    break;
                }
            }
            return !i.hasNext()
                   && getType() == op.getType()
                   && Arrays.equals(data, op.data)
                   && flags == op.flags
                   && aclEquals;
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode() + Arrays.hashCode(data);
        }

        @Override
        public Record toRequestRecord() {
            return new CreateRequest(getPath(), data, acl, flags);
        }

        @Override
        Op withChroot(String path) {
            return new Create(path, data, acl, flags);
        }

        @Override
        void validate() throws KeeperException {
            CreateMode createMode = CreateMode.fromFlag(flags);
            PathUtils.validatePath(getPath(), createMode.isSequential());
            EphemeralType.validateTTL(createMode, -1);
        }

    }

    public static class CreateTTL extends Create {

        private final long ttl;

        private CreateTTL(String path, byte[] data, List<ACL> acl, int flags, long ttl) {
            super(path, data, acl, flags);
            this.ttl = ttl;
        }

        private CreateTTL(String path, byte[] data, List<ACL> acl, CreateMode createMode, long ttl) {
            super(path, data, acl, createMode);
            this.ttl = ttl;
        }

        @Override
        public boolean equals(Object o) {
            return super.equals(o) && (o instanceof CreateTTL) && (ttl == ((CreateTTL) o).ttl);
        }

        @Override
        public int hashCode() {
            return super.hashCode() + (int) (ttl ^ (ttl >>> 32));
        }

        @Override
        public Record toRequestRecord() {
            return new CreateTTLRequest(getPath(), data, acl, flags, ttl);
        }

        @Override
        Op withChroot(String path) {
            return new CreateTTL(path, data, acl, flags, ttl);
        }

        @Override
        void validate() throws KeeperException {
            CreateMode createMode = CreateMode.fromFlag(flags);
            PathUtils.validatePath(getPath(), createMode.isSequential());
            EphemeralType.validateTTL(createMode, ttl);
        }

    }

    public static class Delete extends Op {

        private int version;

        private Delete(String path, int version) {
            super(ZooDefs.OpCode.delete, path, OpKind.TRANSACTION);
            this.version = version;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof Delete)) {
                return false;
            }

            Delete op = (Delete) o;

            return getType() == op.getType() && version == op.version && getPath().equals(op.getPath());
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode() + version;
        }

        @Override
        public Record toRequestRecord() {
            return new DeleteRequest(getPath(), version);
        }

        @Override
        Op withChroot(String path) {
            return new Delete(path, version);
        }

    }

    public static class SetData extends Op {

        private byte[] data;
        private int version;

        private SetData(String path, byte[] data, int version) {
            super(ZooDefs.OpCode.setData, path, OpKind.TRANSACTION);
            this.data = data;
            this.version = version;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof SetData)) {
                return false;
            }

            SetData op = (SetData) o;

            return getType() == op.getType()
                   && version == op.version
                   && getPath().equals(op.getPath())
                   && Arrays.equals(data, op.data);
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode() + Arrays.hashCode(data) + version;
        }

        @Override
        public Record toRequestRecord() {
            return new SetDataRequest(getPath(), data, version);
        }

        @Override
        Op withChroot(String path) {
            return new SetData(path, data, version);
        }

    }

    public static class Check extends Op {

        private int version;

        private Check(String path, int version) {
            super(ZooDefs.OpCode.check, path, OpKind.TRANSACTION);
            this.version = version;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof Check)) {
                return false;
            }

            Check op = (Check) o;

            return getType() == op.getType() && getPath().equals(op.getPath()) && version == op.version;
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode() + version;
        }

        @Override
        public Record toRequestRecord() {
            return new CheckVersionRequest(getPath(), version);
        }

        @Override
        Op withChroot(String path) {
            return new Check(path, version);
        }

    }

    public static class GetChildren extends Op {

        GetChildren(String path) {
            super(ZooDefs.OpCode.getChildren, path, OpKind.READ);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof GetChildren)) {
                return false;
            }

            GetChildren op = (GetChildren) o;

            return getType() == op.getType() && getPath().equals(op.getPath());
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode();
        }

        @Override
        public Record toRequestRecord() {
            return new GetChildrenRequest(getPath(), false);
        }

        @Override
        Op withChroot(String path) {
            return new GetChildren(path);
        }

    }

    public static class GetData extends Op {

        GetData(String path) {
            super(ZooDefs.OpCode.getData, path, OpKind.READ);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof GetData)) {
                return false;
            }

            GetData op = (GetData) o;

            return getType() == op.getType() && getPath().equals(op.getPath());
        }

        @Override
        public int hashCode() {
            return getType() + getPath().hashCode();
        }

        @Override
        public Record toRequestRecord() {
            return new GetDataRequest(getPath(), false);
        }

        @Override
        Op withChroot(String path) {
            return new GetData(path);
        }

    }

}
