/*
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.plc4x.edgent;

import com.google.gson.JsonObject;
import org.apache.edgent.function.Consumer;
import org.apache.edgent.function.Function;
import org.apache.edgent.function.Supplier;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.apache.plc4x.java.api.types.PlcClientDatatype;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

/**
 * WIP - A plc4x Apache Edgent {@link Supplier} and {@link Consumer} connector factory.
 * <p>
 * TODO:
 * Are there cases where a single logical poll would want to read from
 * multiple addrs/data (of different types) from a device and bundle the values
 * into a single TStream tuple (e.g., a JsonObject)?  How would we support that?
 * Is there a similar need for writing to multiple addrs/values on a device?
 * Ah... NOTE: plc4c "batch" requests are coming and will help to address this.
 *
 * <p>
 * Sample use to read plc device data into an Edgent TStream:
 * <pre>{@code
 * PlcConnection plcConnection = new PlcDriverManager().getConnection("s7://192.168.0.1/0/0");
 * plcConnection.connect();
 * PlcConnectionAdapter adapter = new PlcConnectionAdapter(plcConnection));
 *
 * DirectProvider dp = new DirectProvider();
 * Topology top = dp.newTopology();
 * TStream<Byte> stream = top.poll(PlcFunctions.byteSupplier(adapter, "INPUTS/0", 1, TimeUnit.SECONDS);
 * stream.print();
 * dp.submit(top);
 * }</pre>
 * <p>
 * Sample use to write Edgent TStream data to a plc device:
 * <pre>{@code
 * PlcConnection plcConnection = new PlcDriverManager().getConnection("s7://192.168.0.1/0/0");
 * plcConnection.connect();
 * PlcConnectionAdapter adapter = new PlcConnectionAdapter(plcConnection);
 *
 * DirectProvider dp = new DirectProvider();
 * Topology top = dp.newTopology();
 *
 * TStream<Byte> stream = ...
 * stream.print();
 * TSink<Byte> sink = stream.sink(PlcFunctions.byteConsumer(adapter, "OUTPUTS/0"));
 *
 * dp.submit(top);
 * }</pre>
 */
public class PlcFunctions {

    private PlcFunctions() {
        throw new IllegalStateException("Utility class!");
    }

    /**
     * Create a new Edgent {@link Supplier} to read data from the
     * plc device.
     * <p>
     * Every call to the returned {@link Supplier#get()} reads a
     * new data value from the plc device address and connection
     * associated with the {@code PlcConnectionAdapter}.
     * <p>
     *
     * @param adapter    the @{link PlcConnectionAdapter}
     * @param addressStr the plc device address string
     * @return the {@code Supplier<T>}
     * <p>
     * // TODO: No need to import the Topology module for just this comment ... I think
     * //see org.apache.edgent.topology.Topology#poll(Supplier, long, java.util.concurrent.TimeUnit)
     */
    /*public static Supplier<byte[]> rawSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(byte[].class, PlcClientDatatype.RAW, addressStr);
    }

    public static Supplier<Object> objectSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Object.class, PlcClientDatatype.OBJECT, addressStr);
    }*/

    public static Supplier<Boolean> booleanSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Boolean.class, PlcClientDatatype.BOOLEAN, addressStr);
    }

    public static Supplier<List<Boolean>> booleanListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Boolean.class, PlcClientDatatype.BOOLEAN, addressStr);
    }

    public static Supplier<Byte> byteSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Byte.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<List<Byte>> byteListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Byte.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<Short> shortSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Short.class, PlcClientDatatype.SHORT, addressStr);
    }

    public static Supplier<List<Short>> shortListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Short.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<Integer> integerSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Integer.class, PlcClientDatatype.INTEGER, addressStr);
    }

    public static Supplier<List<Integer>> integerListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Integer.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<Long> longSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Long.class, PlcClientDatatype.LONG, addressStr);
    }

    public static Supplier<List<Long>> longListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Long.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<Float> floatSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Float.class, PlcClientDatatype.FLOAT, addressStr);
    }

    public static Supplier<List<Float>> floatListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Float.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<Double> doubleSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(Double.class, PlcClientDatatype.DOUBLE, addressStr);
    }

    public static Supplier<List<Double>> doubleListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(Double.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<String> stringSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(String.class, PlcClientDatatype.STRING, addressStr);
    }

    public static Supplier<List<String>> stringListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(String.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<LocalTime> timeSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(LocalTime.class, PlcClientDatatype.TIME, addressStr);
    }

    public static Supplier<List<LocalTime>> timeListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(LocalTime.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<LocalDate> dateSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(LocalDate.class, PlcClientDatatype.DATE, addressStr);
    }

    public static Supplier<List<LocalDate>> dateListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(LocalDate.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<LocalDateTime> dateTimeSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newSupplier(LocalDateTime.class, PlcClientDatatype.DATE_TIME, addressStr);
    }

    public static Supplier<List<LocalDateTime>> dateTimeListSupplier(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newListSupplier(LocalDateTime.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Supplier<PlcReadResponse> batchSupplier(PlcConnectionAdapter adapter, PlcReadRequest readRequest) {
        return adapter.newSupplier(readRequest);
    }

    /**
     * Create a new Edgent {@link Consumer} to write data to the
     * plc device.
     * <p>
     * Every call to the returned {@link Consumer#accept(Object)}
     * writes the value to the the device address and connection
     * associated with the {@code PlcConnectionAdapter}.
     *
     * @param adapter    the @{link PlcConnectionAdapter}
     * @param addressStr the plc device address string
     * @return the {@code Consumer<T>}
     * <p>
     * // TODO: No need to import the Topology module for just this comment ... I think
     * //see org.apache.edgent.topology.TStream#sink(Consumer)
     */
    /*public static Consumer<byte[]> rawConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(byte[].class, PlcClientDatatype.RAW, addressStr);
    }

    public static Consumer<Object> objectConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Object.class, PlcClientDatatype.OBJECT, addressStr);
    }*/

    public static Consumer<Boolean> booleanConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Boolean.class, PlcClientDatatype.BOOLEAN, addressStr);
    }

    public static Consumer<Byte> byteConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Byte.class, PlcClientDatatype.BYTE, addressStr);
    }

    public static Consumer<Short> shortConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Short.class, PlcClientDatatype.SHORT, addressStr);
    }

    public static Consumer<Integer> integerConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Integer.class, PlcClientDatatype.INTEGER, addressStr);
    }

    public static Consumer<Long> longConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Long.class, PlcClientDatatype.LONG, addressStr);
    }

    public static Consumer<Float> floatConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Float.class, PlcClientDatatype.FLOAT, addressStr);
    }

    public static Consumer<Double> doubleConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(Double.class, PlcClientDatatype.DOUBLE, addressStr);
    }

    public static Consumer<String> stringConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(String.class, PlcClientDatatype.STRING, addressStr);
    }

    public static Consumer<LocalTime> timeConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(LocalTime.class, PlcClientDatatype.TIME, addressStr);
    }

    public static Consumer<LocalDate> dateConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(LocalDate.class, PlcClientDatatype.DATE, addressStr);
    }

    public static Consumer<LocalDateTime> dateTimeConsumer(PlcConnectionAdapter adapter, String addressStr) {
        return adapter.newJsonConsumer(LocalDateTime.class, PlcClientDatatype.DATE_TIME, addressStr);
    }

    /**
     * Create a new Edgent {@link Consumer} to write data to the
     * plc device.
     * <p>
     * TODO: Is it premature to supply this?
     * <p>
     * Every call to the returned {@link Consumer#accept(Object)}
     * <ul>
     * <li>calls {@code addressFn} to get the device address string</li>
     * <li>calls {@code valueFn} to get the {@code T} to write</li>
     * <li>writes the value to the device address using the connection
     * associated with the {@code PlcConnectionAdapter}.</li>
     * </ul>
     *
     * @param adapter   the @{link PlcConnectionAdapter}
     * @param addressFn {@code Function} the returns a device {@code PlcField} from a {@code JsonObject}
     * @param valueFn   {@code Function} the returns a {@code Value} from a {@code JsonObject}
     * @return the {@code Consumer<JsonObject>}
     * <p>
     * // TODO: No need to import the Topology module for just this comment ... I think
     * //see org.apache.edgent.topology.TStream#sink(Consumer)
     */
    public static Consumer<JsonObject> booleanConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Boolean> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.BOOLEAN, addressFn, valueFn);
    }

    public static Consumer<JsonObject> byteConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Byte> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.BYTE, addressFn, valueFn);
    }

    public static Consumer<JsonObject> shortConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Short> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.SHORT, addressFn, valueFn);
    }

    public static Consumer<JsonObject> integerConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Integer> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.INTEGER, addressFn, valueFn);
    }

    public static Consumer<JsonObject> longConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Long> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.LONG, addressFn, valueFn);
    }

    public static Consumer<JsonObject> floatConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Float> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.FLOAT, addressFn, valueFn);
    }

    public static Consumer<JsonObject> doubleConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, Double> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.DOUBLE, addressFn, valueFn);
    }

    public static Consumer<JsonObject> stringConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, String> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.STRING, addressFn, valueFn);
    }

    public static Consumer<JsonObject> timeConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, LocalTime> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.TIME, addressFn, valueFn);
    }

    public static Consumer<JsonObject> dateConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, LocalDate> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.DATE, addressFn, valueFn);
    }

    public static Consumer<JsonObject> dateTimeConsumer(PlcConnectionAdapter adapter, Function<JsonObject, String> addressFn, Function<JsonObject, LocalDateTime> valueFn) {
        return adapter.newJsonConsumer(PlcClientDatatype.DATE_TIME, addressFn, valueFn);
    }

}
