/*******************************************************************************
 * Copyright (c) 2008, 2012 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Connexta, LLC - evaluation cache implementation
 *******************************************************************************/
package org.eclipse.osgi.internal.permadmin;

import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.osgi.internal.permadmin.SecurityRow.Decision;
import org.osgi.service.condpermadmin.Condition;

public class SecurityTable extends PermissionCollection {
	private static final long serialVersionUID = -1800193310096318060L;
	static final int GRANTED = 0x0001;
	static final int DENIED = 0x0002;
	static final int ABSTAIN = 0x0004;
	static final int POSTPONED = 0x0008;

	private static final int MUTABLE = 0x0016;

	private final SecurityRow[] rows;
	private final SecurityAdmin securityAdmin;

	private final transient Map<EvaluationCacheKey, Integer> evaluationCache = new ConcurrentHashMap<>(10000);

	public SecurityTable(SecurityAdmin securityAdmin, SecurityRow[] rows) {
		if (rows == null)
			throw new NullPointerException("rows cannot be null!!"); //$NON-NLS-1$
		this.rows = rows;
		this.securityAdmin = securityAdmin;
	}

	boolean isEmpty() {
		return rows.length == 0;
	}

	int evaluate(BundlePermissions bundlePermissions, Permission permission) {
		if (bundlePermissions == null) {
			return ABSTAIN;
		}
		EvaluationCacheKey evaluationCacheKey = new EvaluationCacheKey(bundlePermissions, permission);
		if (isEmpty()) {
			evaluationCache.put(evaluationCacheKey, ABSTAIN);
			return ABSTAIN;
		}

		//can't short-circuit early, so try cache
		Integer result = evaluationCache.get(evaluationCacheKey);
		boolean hasMutable = false;
		if (result != null) {
			hasMutable = (result & MUTABLE) == MUTABLE;
			if (!hasMutable) {
				return result;
			}
		}
		//cache miss or has mutable rows
		boolean postponed = false;
		Decision[] results = new Decision[rows.length];
		int immediateDecisionIdx = -1;
		// evaluate each row
		for (int i = 0; i < rows.length && immediateDecisionIdx == -1; i++) {
			if (result == null) {
				//check all conditions for any that are mutable, this will turn off the cache
				hasMutable |= checkMutable(bundlePermissions, evaluationCacheKey, rows[i]);
			}
			try {
				results[i] = rows[i].evaluate(bundlePermissions, permission);
			} catch (Exception e) {
				// TODO log?
				results[i] = SecurityRow.DECISION_ABSTAIN;
			}
			if ((results[i].decision & ABSTAIN) == ABSTAIN)
				continue; // ignore this row and continue to next row
			if ((results[i].decision & POSTPONED) == POSTPONED) {
				// row is postponed; we can no longer return quickly on a denied decision
				postponed = true;
				continue; // continue to next row
			}
			if (!postponed) {
				// no postpones encountered yet; we can return the decision quickly
				if (!hasMutable) {
					evaluationCache.put(evaluationCacheKey, results[i].decision);
				}
				return results[i].decision; // return GRANTED or DENIED
			}
			// got an immediate answer; but it is after a postponed condition.
			// no need to process the rest of the rows
			immediateDecisionIdx = i;
		}
		Integer immediateDecision = handlePostponedConditions(evaluationCacheKey, hasMutable, postponed, results, immediateDecisionIdx);
		if (immediateDecision != null)
			return immediateDecision;
		int finalDecision = postponed ? POSTPONED : ABSTAIN;
		if (!hasMutable && (finalDecision & POSTPONED) != POSTPONED) {
			evaluationCache.put(evaluationCacheKey, finalDecision);
		}
		return finalDecision;
	}

	private boolean checkMutable(BundlePermissions bundlePermissions, EvaluationCacheKey evaluationCacheKey, SecurityRow row) {
		Condition[] conditions = row.getConditions(bundlePermissions);
		if (conditions != null) {
			for (Condition condition : conditions) {
				if (condition != null && condition.isMutable()) {
					evaluationCache.put(evaluationCacheKey, MUTABLE);
					return true;
				}
			}
		}
		return false;
	}

	private Integer handlePostponedConditions(EvaluationCacheKey evaluationCacheKey, boolean hasMutable, boolean postponed, Decision[] results, int immediateDecisionIdx) {
		if (postponed) {
			int immediateDecision = immediateDecisionIdx < 0 ? DENIED : results[immediateDecisionIdx].decision;
			// iterate over all postponed conditions;
			// if they all provide the same decision as the immediate decision then return the immediate decision
			boolean allSameDecision = true;
			int i = immediateDecisionIdx < 0 ? results.length - 1 : immediateDecisionIdx - 1;
			for (; i >= 0 && allSameDecision; i--) {
				if ((results[i].decision & POSTPONED) == POSTPONED) {
					if ((results[i].decision & immediateDecision) == 0)
						allSameDecision = false;
					else
						results[i] = SecurityRow.DECISION_ABSTAIN; // we can clear postpones with the same decision as the immediate
				}
			}
			if (allSameDecision) {
				if (!hasMutable) {
					evaluationCache.put(evaluationCacheKey, immediateDecision);
				}
				return immediateDecision;
			}

			// we now are forced to postpone; we need to also remember the postponed decisions and
			// the immediate decision if there is one.
			EquinoxSecurityManager equinoxManager = securityAdmin.getSupportedSecurityManager();
			if (equinoxManager == null) {
				// TODO this is really an error condition.
				// This should never happen.  We checked for a supported manager when the row was postponed
				if (!hasMutable) {
					evaluationCache.put(evaluationCacheKey, ABSTAIN);
				}
				return ABSTAIN;
			}
			equinoxManager.addConditionsForDomain(results);
		}
		return null;
	}

	void clearEvaluationCache() {
		evaluationCache.clear();
	}

	SecurityRow getRow(int i) {
		return rows.length <= i || i < 0 ? null : rows[i];
	}

	SecurityRow getRow(String name) {
		for (SecurityRow row : rows) {
			if (name.equals(row.getName())) {
				return row;
			}
		}
		return null;
	}

	SecurityRow[] getRows() {
		return rows;
	}

	String[] getEncodedRows() {
		String[] encoded = new String[rows.length];
		for (int i = 0; i < rows.length; i++)
			encoded[i] = rows[i].getEncoded();
		return encoded;
	}

	@Override
	public void add(Permission permission) {
		throw new SecurityException();
	}

	@Override
	public Enumeration<Permission> elements() {
		return BundlePermissions.EMPTY_ENUMERATION;
	}

	@Override
	public boolean implies(Permission permission) {
		return (evaluate(null, permission) & SecurityTable.GRANTED) != 0;
	}
}
