/*
 * Copyright 2011 Google Inc.
 * Copyright 2015 Andreas Schildbach
 *
 * 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.
 */

package org.bitcoinj.core;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * Alerts are signed messages that are broadcast on the peer-to-peer network if they match a hard-coded signing key.
 * The private keys are held by a small group of core Bitcoin developers, and alerts may be broadcast in the event of
 * an available upgrade or a serious network problem. Alerts have an expiration time, data that specifies what
 * set of software versions it matches and the ability to cancel them by broadcasting another type of alert.<p>
 *
 * The right course of action on receiving an alert is usually to either ensure a human will see it (display on screen,
 * log, email), or if you decide to use alerts for notifications that are specific to your app in some way, to parse it.
 * For example, you could treat it as an upgrade notification specific to your app. Satoshi designed alerts to ensure
 * that software upgrades could be distributed independently of a hard-coded website, in order to allow everything to
 * be purely peer-to-peer. You don't have to use this of course, and indeed it often makes more sense not to.<p>
 *     
 * <p>Before doing anything with an alert, you should check {@link AlertMessage#isSignatureValid()}.</p>
 * 
 * <p>Instances of this class are not safe for use by multiple threads.</p>
 */
public class AlertMessage extends Message {
    private byte[] content;
    private byte[] signature;

    // See the getters for documentation of what each field means.
    private long version = 1;
    private Date relayUntil;
    private Date expiration;
    private long id;
    private long cancel;
    private long minVer, maxVer;
    private long priority;
    private String comment, statusBar, reserved;

    // Chosen arbitrarily to avoid memory blowups.
    private static final long MAX_SET_SIZE = 100;

    public AlertMessage(NetworkParameters params, byte[] payloadBytes) throws ProtocolException {
        super(params, payloadBytes, 0);
    }

    @Override
    public String toString() {
        return "ALERT: " + getStatusBar();
    }

    @Override
    protected void parse() throws ProtocolException {
        // Alerts are formatted in two levels. The top level contains two byte arrays: a signature, and a serialized
        // data structure containing the actual alert data.
        int startPos = cursor;
        content = readByteArray();
        signature = readByteArray();
        // Now we need to parse out the contents of the embedded structure. Rewind back to the start of the message.
        cursor = startPos;
        readVarInt();  // Skip the length field on the content array.
        // We're inside the embedded structure.
        version = readUint32();
        // Read the timestamps. Bitcoin uses seconds since the epoch.
        relayUntil = new Date(readUint64().longValue() * 1000);
        expiration = new Date(readUint64().longValue() * 1000);
        id = readUint32();
        cancel = readUint32();
        // Sets are serialized as <len><item><item><item>....
        long cancelSetSize = readVarInt();
        if (cancelSetSize < 0 || cancelSetSize > MAX_SET_SIZE) {
            throw new ProtocolException("Bad cancel set size: " + cancelSetSize);
        }
        // Using a hashset here is very inefficient given that this will normally be only one item. But Java doesn't
        // make it easy to do better. What we really want is just an array-backed set.
        Set<Long> cancelSet = new HashSet<>((int) cancelSetSize);
        for (long i = 0; i < cancelSetSize; i++) {
            cancelSet.add(readUint32());
        }
        minVer = readUint32();
        maxVer = readUint32();
        // Read the subver matching set.
        long subverSetSize = readVarInt();
        if (subverSetSize < 0 || subverSetSize > MAX_SET_SIZE) {
            throw new ProtocolException("Bad subver set size: " + subverSetSize);
        }
        Set<String> matchingSubVers = new HashSet<>((int) subverSetSize);
        for (long i = 0; i < subverSetSize; i++) {
            matchingSubVers.add(readStr());
        }
        priority = readUint32();
        comment = readStr();
        statusBar = readStr();
        reserved = readStr();

        length = cursor - offset;
    }

    /**
     * Returns true if the digital signature attached to the message verifies. Don't do anything with the alert if it
     * doesn't verify, because that would allow arbitrary attackers to spam your users.
     */
    public boolean isSignatureValid() {
        return ECKey.verify(Sha256Hash.hashTwice(content), signature, params.getAlertSigningKey());
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //  Field accessors.

    /**
     * The time at which the alert should stop being broadcast across the network. Note that you can still receive
     * the alert after this time from other nodes if the alert still applies to them or to you.
     */
    public Date getRelayUntil() {
        return relayUntil;
    }

    public void setRelayUntil(Date relayUntil) {
        this.relayUntil = relayUntil;
    }

    /**
     * The time at which the alert ceases to be relevant. It should not be presented to the user or app administrator
     * after this time.
     */
    public Date getExpiration() {
        return expiration;
    }

    public void setExpiration(Date expiration) {
        this.expiration = expiration;
    }

    /**
     * The numeric identifier of this alert. Each alert should have a unique ID, but the signer can choose any number.
     * If an alert is broadcast with a cancel field higher than this ID, this alert is considered cancelled.
     * @return uint32
     */
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    /**
     * A marker that results in any alerts with an ID lower than this value to be considered cancelled.
     * @return uint32
     */
    public long getCancel() {
        return cancel;
    }

    public void setCancel(long cancel) {
        this.cancel = cancel;
    }

    /**
     * The inclusive lower bound on software versions that are considered for the purposes of this alert. Bitcoin Core
     * compares this against a protocol version field, but as long as the subVer field is used to restrict it your
     * alerts could use any version numbers.
     * @return uint32
     */
    public long getMinVer() {
        return minVer;
    }

    public void setMinVer(long minVer) {
        this.minVer = minVer;
    }

    /**
     * The inclusive upper bound on software versions considered for the purposes of this alert. Bitcoin Core
     * compares this against a protocol version field, but as long as the subVer field is used to restrict it your
     * alerts could use any version numbers.
     */
    public long getMaxVer() {
        return maxVer;
    }

    public void setMaxVer(long maxVer) {
        this.maxVer = maxVer;
    }

    /**
     * Provides an integer ordering amongst simultaneously active alerts.
     * @return uint32
     */
    public long getPriority() {
        return priority;
    }

    public void setPriority(long priority) {
        this.priority = priority;
    }

    /**
     * This field is unused. It is presumably intended for the author of the alert to provide a justification for it
     * visible to protocol developers but not users.
     */
    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    /**
     * A string that is intended to display in the status bar of Bitcoin Core's GUI client. It contains the user-visible
     * message. English only.
     */
    public String getStatusBar() {
        return statusBar;
    }

    public void setStatusBar(String statusBar) {
        this.statusBar = statusBar;
    }

    /**
     * This field is never used.
     */
    public String getReserved() {
        return reserved;
    }

    public void setReserved(String reserved) {
        this.reserved = reserved;
    }
    
    public long getVersion() {
        return version;
    }
}
