/*
 * ICESOFT COMMERCIAL SOURCE CODE LICENSE V 1.1
 *
 * The contents of this file are subject to the ICEsoft Commercial Source
 * Code License Agreement V1.1 (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.icesoft.com/license/commercial-source-v1.1.html
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * Copyright 2009-2015 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 */

package com.icesoft.push;

import org.icepush.NotifyBackURI;

import java.io.Serializable;
import java.util.Timer;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

public class PushID
extends org.icepush.PushID
implements Serializable {
    private static final long serialVersionUID = -8715751288425858051L;

    private static final Logger LOGGER = Logger.getLogger(PushID.class.getName());

    private transient final ReentrantLock confirmationTimeoutLock = new ReentrantLock();
    private transient final ReentrantLock expiryTimeoutLock = new ReentrantLock();
    private transient final UUID uuid;

    /*
     * The confirmationTimeoutTime should be transient as each instance upon a priority push request should be able to
     * determine the value of this.
     */
    private transient long confirmationTimeoutTime = -1;
    private long expiryTimeoutSequenceNumber = -1;

    /*
     * The lastHandledByLocal should be transient as the value is specific to the instance.
     */
    private transient boolean lastHandledByLocal = false;

    PushID(
        final PushID pushID, final String groupName, final long pushIDTimeout, final long cloudPushIDTimeout,
        final Timer timeoutTimer, final long minCloudPushInterval, final LocalPushGroupManager localPushGroupManager,
        final UUID uuid) {

        super(
            pushID.getID(), groupName, pushIDTimeout, cloudPushIDTimeout, timeoutTimer, minCloudPushInterval,
            localPushGroupManager);
        ((Status)getStatus()).confirmationTimeoutSequenceNumber =
            ((Status)pushID.getStatus()).confirmationTimeoutSequenceNumber;
        this.confirmationTimeoutTime = pushID.confirmationTimeoutTime;
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(
                Level.FINE,
                "New confirmation timeout time '" + this.confirmationTimeoutTime + "' for '" + getID() + "' " +
                    "[Constructor :: " + (this.confirmationTimeoutTime - System.currentTimeMillis()) + " from now]");
        }
        this.uuid = uuid;
        setNotifyBackURI(pushID.getNotifyBackURI());
    }

    PushID(
        final String id, final String groupName, final long pushIDTimeout, final long cloudPushIDTimeout,
        final Timer timeoutTimer, final long minCloudPushInterval, final LocalPushGroupManager localPushGroupManager,
        final UUID uuid) {

        super(
            id, groupName, pushIDTimeout, cloudPushIDTimeout, timeoutTimer, minCloudPushInterval,
            localPushGroupManager);
        this.uuid = uuid;
    }

    @Override
    public boolean cancelConfirmationTimeout() {
        confirmationTimeoutLock.lock();
        try {
            boolean _cancelled;
            _cancelled = super.cancelConfirmationTimeout();
            clearConfirmationTimeout();
            return _cancelled;
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    @Override
    public boolean cancelExpiryTimeout() {
        expiryTimeoutLock.lock();
        try {
            boolean _cancelled;
            if (_cancelled = super.cancelExpiryTimeout()) {
                expiryTimeoutSequenceNumber = -1;
            }
            return _cancelled;
        } finally {
            expiryTimeoutLock.unlock();
        }
    }

    @Override
    public void setSequenceNumber(final long sequenceNumber) {
        super.setSequenceNumber(sequenceNumber);
        // Should only be invoked by PushGroupManager.recordListen(...)
        setLastHandledByLocal(true);
    }

    @Override
    public boolean startConfirmationTimeout(final long sequenceNumber) {
        confirmationTimeoutLock.lock();
        try {
            return super.startConfirmationTimeout(sequenceNumber);
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    @Override
    public boolean startConfirmationTimeout(final long sequenceNumber, final long timeout) {
        confirmationTimeoutLock.lock();
        try {
            boolean _started = false;
            if (getPushConfiguration() != null) {
                if (getConfirmationTimeout() == null) {
                    if (isLastHandledByLocal()) {
                        Status _status = (Status)getStatus();
                        _status.setConfirmationHandlerUUID(uuid);
                        _status.setConfirmationTimeoutSequenceNumber(sequenceNumber);
                        _started = super.startConfirmationTimeout(sequenceNumber, timeout);
                    } else {
                        long _now = System.currentTimeMillis();
                        confirmationTimeoutTime = _now + timeout;
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(
                                Level.FINE,
                                "New confirmation timeout time: '" + confirmationTimeoutTime + "' for '" + getID() + "' " +
                                    "[Recorded :: " + (confirmationTimeoutTime - _now) + "ms. from now]");
                        }
                    }
                }
            }
            return _started;
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    @Override
    public boolean startExpiryTimeout(final NotifyBackURI notifyBackURI, final long sequenceNumber) {
        expiryTimeoutLock.lock();
        try {
            boolean _started;
            if (_started = super.startExpiryTimeout(notifyBackURI, sequenceNumber)) {
                expiryTimeoutSequenceNumber = sequenceNumber;
            }
            return _started;
        } finally {
            expiryTimeoutLock.unlock();
        }
    }

    @Override
    public String toString() {
        return
            new StringBuilder().
                append("PushID[").
                    append("id: '").append(getID()).append("', ").
                    append("status: '").append(getStatus()).append("'").
                append("]").
                    toString();
    }

    @Override
    protected org.icepush.PushID.ConfirmationTimeout newConfirmationTimeout(
        final NotifyBackURI notifyBackURI, final long timeout) {

        return new ConfirmationTimeout(this, notifyBackURI, timeout);
    }

    @Override
    protected org.icepush.PushID.Status newStatus() {
        return new Status();
    }

    boolean cancelConfirmationTimeoutIfOlder(final long remoteSequenceNumber) {
        confirmationTimeoutLock.lock();
        try {
            boolean _cancelled = false;
            long _localConfirmationTimeoutSequenceNumber = ((Status)getStatus()).getConfirmationTimeoutSequenceNumber();
            if (_localConfirmationTimeoutSequenceNumber != -1 &&
                _localConfirmationTimeoutSequenceNumber < remoteSequenceNumber) {

                NotifyBackURI notifyBackURI = getNotifyBackURI();
                if (notifyBackURI != null) {
                    notifyBackURI.touch();
                }
                _cancelled = cancelConfirmationTimeout();
            }
            return _cancelled;
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    boolean cancelExpiryTimeoutIfOlder(final long sequenceNumber) {
        expiryTimeoutLock.lock();
        try {
            boolean _cancelled = false;
            if (expiryTimeoutSequenceNumber < sequenceNumber) {
                NotifyBackURI notifyBackURI = getNotifyBackURI();
                if (notifyBackURI != null) {
                    notifyBackURI.touch();
                }
                _cancelled = cancelExpiryTimeout();
            }
            return _cancelled;
        } finally {
            expiryTimeoutLock.unlock();
        }
    }

    boolean clearConfirmationTimeoutIfMatching(final long sequenceNumber) {
        confirmationTimeoutLock.lock();
        try {
            boolean _cleared = false;
            if (((Status)getStatus()).getConfirmationTimeoutSequenceNumber() == sequenceNumber) {
                _cleared = clearConfirmationTimeout();
            }
            return _cleared;
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    boolean isLastHandledByLocal() {
        return lastHandledByLocal;
    }

    void restartExpiryTimeoutIfOlder(final long sequenceNumber) {
        expiryTimeoutLock.lock();
        try {
            if (expiryTimeoutSequenceNumber < sequenceNumber) {
                NotifyBackURI notifyBackURI = getNotifyBackURI();
                if (notifyBackURI != null) {
                    notifyBackURI.touch();
                }
                cancelExpiryTimeout();
                startExpiryTimeout(getNotifyBackURI(), sequenceNumber);
            }
        } finally {
            expiryTimeoutLock.unlock();
        }
    }

    void resumeConfirmationTimeout() {
        confirmationTimeoutLock.lock();
        try {
            if (getPushConfiguration() != null) {
                if (getConfirmationTimeout() == null) {
                    Status _status = (Status)getStatus();
                    _status.setConfirmationHandlerUUID(uuid);
                    long _remainingTimeout = Math.max(confirmationTimeoutTime - System.currentTimeMillis(), 0);
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.log(
                            Level.FINE,
                            "Resume confirmation timeout for '" + getID() + "' " +
                                "[remaining timeout: '" + _remainingTimeout + "']");
                    }
                    super.startConfirmationTimeout(_status.getConfirmationTimeoutSequenceNumber(), _remainingTimeout);
                }
            }
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    void setLastHandledByLocal(final boolean lastHandledByLocal) {
        this.lastHandledByLocal = lastHandledByLocal;
    }

    private boolean clearConfirmationTimeout() {
        confirmationTimeoutLock.lock();
        try {
            Status _status = (Status)getStatus();
            if (confirmationTimeoutTime != -1 || _status.getConfirmationTimeoutSequenceNumber() != -1 ||
                _status.getConfirmationHandlerUUID() != null) {

                confirmationTimeoutTime = -1;
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.log(
                        Level.FINE,
                        "New confirmation timeout time: '" + confirmationTimeoutTime + "' for '" + getID() + "' " +
                            "[Cleared]");
                }
                _status.setConfirmationTimeoutSequenceNumber(-1);
                _status.setConfirmationHandlerUUID(null);
                return true;
            }
            return false;
        } finally {
            confirmationTimeoutLock.unlock();
        }
    }

    private static class ConfirmationTimeout
    extends org.icepush.PushID.ConfirmationTimeout {
        private PushID pushID;

        private ConfirmationTimeout(final PushID pushID, final NotifyBackURI notifyBackURI, final long timeout) {
            super(pushID, notifyBackURI, timeout);
            this.pushID = pushID;
        }

        @Override
        public void run() {
            super.run();
            Status _status = (Status)pushID.getStatus();
            _status.setCloudPushSequenceNumber(_status.getConfirmationTimeoutSequenceNumber());
        }
    }

    public static class Status
    extends org.icepush.PushID.Status
    implements Serializable {
        private static final long serialVersionUID = 4630028856848762054L;

        private long cloudPushSequenceNumber = -1;
        private UUID confirmationHandlerUUID;
        private long confirmationTimeoutSequenceNumber = -1;

        Status(final Status status) {
            super(status);
            setCloudPushSequenceNumber(status.getCloudPushSequenceNumber());
            setConfirmationHandlerUUID(status.getConfirmationHandlerUUID());
            setConfirmationTimeoutSequenceNumber(status.getConfirmationTimeoutSequenceNumber());
        }

        private Status() {
            super();
        }

        public long getCloudPushSequenceNumber() {
            return cloudPushSequenceNumber;
        }

        public UUID getConfirmationHandlerUUID() {
            return confirmationHandlerUUID;
        }

        public long getConfirmationTimeoutSequenceNumber() {
            return confirmationTimeoutSequenceNumber;
        }

        @Override
        public String toString() {
            return
                new StringBuilder().
                    append("PushID.Status[").append(membersAsString()).append("]").
                        toString();
        }

        protected String membersAsString() {
            return
                new StringBuilder().
                    append("cloudPushSequenceNumber: ").
                        append("'").append(getCloudPushSequenceNumber()).append("', ").
                    append("confirmationHandlerUUID: ").
                        append("'").append(getConfirmationHandlerUUID()).append("', ").
                    append("confirmationTimeoutSequenceNumber: ").
                        append("'").append(getConfirmationTimeoutSequenceNumber()).append("', ").
                    append(super.membersAsString()).
                            toString();
        }

        void setCloudPushSequenceNumber(final long cloudPushSequenceNumber) {
            this.cloudPushSequenceNumber = cloudPushSequenceNumber;
        }

        void setConfirmationHandlerUUID(final UUID confirmationHandlerUUID) {
            this.confirmationHandlerUUID = confirmationHandlerUUID;
        }

        void setConfirmationTimeoutSequenceNumber(final long confirmationTimeoutSequenceNumber) {
            this.confirmationTimeoutSequenceNumber = confirmationTimeoutSequenceNumber;
        }
    }
}
