/*
 * 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.cassandra.cql3.ast;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;

import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.BytesType;

/**
 * Represents {@code foo.bar.baz} syntax.  Mainly used for UDT field access ({@code col_udt.foo}) and Accord LET access ({@code row1.foo}).
 */
public class Reference implements ReferenceExpression
{
    public final List<ReferenceExpression> path;

    private Reference(List<ReferenceExpression> path)
    {
        if (path.isEmpty())
            throw new IllegalArgumentException("Reference may not be empty");
        this.path = path;
        if (path.stream().anyMatch(e -> e.toCQL().contains("\"\"")))
            new Throwable().printStackTrace();
    }

    public static Reference of(ReferenceExpression top)
    {
        return new Reference(Collections.singletonList(Objects.requireNonNull(top)));
    }

    public static Reference of(ReferenceExpression top, ReferenceExpression next)
    {
        return new Reference(List.of(top, next));
    }

    @Override
    public AbstractType<?> type()
    {
        return path.get(path.size() - 1).type();
    }

    public Builder unbuild()
    {
        return new Builder(path);
    }

    public Reference add(String symbol, AbstractType<?> subType)
    {
        return unbuild().add(symbol, subType).build();
    }

    public Reference add(Expression expression)
    {
        if (expression instanceof Reference)
        {
            Reference other = (Reference) expression;
            List<ReferenceExpression> path = new ArrayList<>(this.path.size() + other.path.size());
            path.addAll(this.path);
            path.addAll(other.path);
            return new Reference(path);
        }
        else if (expression instanceof Symbol)
        {
            return unbuild().add((Symbol) expression).build();
        }
        else
        {
            return unbuild().add(expression.name(), expression.type()).build();
        }
    }

    public Reference lastAsCollection(Function<ReferenceExpression, CollectionAccess> fn)
    {
        List<ReferenceExpression> path = new ArrayList<>(this.path.size());
        for (int i = 0; i < this.path.size() - 1; i++)
            path.add(this.path.get(i));
        ReferenceExpression last = this.path.get(this.path.size() - 1);
        path.add(Objects.requireNonNull(fn.apply(last)));
        return new Reference(path);
    }

    @Override
    public void toCQL(StringBuilder sb, CQLFormatter formatter)
    {
        path.forEach(p -> {
            p.toCQL(sb, formatter);
            sb.append('.');
        });
        sb.setLength(sb.length() - 1); // last .
    }

    @Override
    public Stream<? extends Element> stream()
    {
        return path.stream();
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Reference elements = (Reference) o;
        return Objects.equals(path, elements.path);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(path);
    }

    @Override
    public String toString()
    {
        return toCQL();
    }

    @Override
    public Symbol column()
    {
        return path.get(0).column();
    }

    public static class Builder
    {
        private final List<ReferenceExpression> path;

        public Builder()
        {
            path = new ArrayList<>();
        }

        private Builder(List<ReferenceExpression> path)
        {
            this.path = new ArrayList<>(path.size() + 1);
            this.path.addAll(path);
        }

        public Builder add(String name)
        {
            return add(new Symbol(name, BytesType.instance));
        }

        public Builder add(String symbol, AbstractType<?> subType)
        {
            return add(new Symbol(symbol, subType));
        }

        public Builder add(Symbol symbol)
        {
            path.add(Objects.requireNonNull(symbol));
            return this;
        }

        public Builder add(String[] paths)
        {
            for (String name : paths)
                path.add(new Symbol(name, BytesType.instance));
            return this;
        }

        public Reference build()
        {
            switch (path.size())
            {
                case 0: throw new IllegalStateException("Unable to make a Reference from an empty path");
                case 1: return Reference.of(path.get(0));
                default: return new Reference(ImmutableList.copyOf(path));
            }
        }
    }
}
